C/C++中按值傳遞比按地址傳遞更快嗎, 引用呢?

effective c++ P12

對於內置類型而言

pass-by-value 通常比 pass-by-reference高效

void f1(int i) {}

void f2(int* i) {}

void f3(int i) {}

int main()
{
int x = 5;
f1(x);
f2(x);
f3(x);
}

所以它的意思是 通常來說 效率是 f3 &> f1 &> f2 ?

所以C99中增加了引用這個特性?

======================

已得到答案...引用就是指針的語法糖 const *

剛看了下彙編...P和R沒區別...

那麼書上的意思應該是movl 比 leaq的效率低...?

movl -4(%rbp), %eax
movl %eax, %edi
call _Z2f1i
leaq -4(%rbp), %rax
movq %rax, %rdi
call _Z2f2Pi
leaq -4(%rbp), %rax
movq %rax, %rdi
call _Z2f3Ri

======================

由這個問題想到的,通常來講內置類型傳值效率高。

對象類型傳引用效率高,所以Java語言設計成了基本類型傳值,

"引用類型"=&>"傳引用"的設計思想?

然後Java其實只有值傳遞, 所以傳進來的應該是T* const類型?


傳值和引用的區別最顯而易見的是多了拷貝,而拷貝對於trivially copyable的對象而言幾乎沒有額外開銷

討論單條指令的效率沒有太大意義, 傳const還是還是傳值,應該首先考慮程序的需求,程序的邏輯在此要不要拷貝,要不要修改參數,性能方面的問題應該留到性能測試上說


寫在前面,題主問的是內置類型,那麼,基本上就是int char long ptr這類數據,我的回答基於這個大前提來討論,所以不要再提什麼構造函數之類的內容了。

引用和按指針傳遞,在彙編級別上應該是一樣的,所以你的代碼里f2和f3在彙編層面上應該是一樣的,所以性能上應該沒有差別。

至於值傳遞和引用傳遞的性能差別,我覺得不應該是寫C++代碼該關注的地方。

如果真要深入分析這個問題,在不開任何優化的情況下:

mov和lea指令速度是有可能有差異的,現代的x86架構的CPU里,開啟流水線不開啟HT的情況下,lea可能比mov要快,或者基本上差不多,我查到的信息里:

Ryzen架構下:

MOV r,m的latency是3,throughput是1/2;

LEA r32/64,[m]的latency是2,throughput是1/4;

Haswell架構下:

MOV r32/64,m的latency是2,throughput是1/2;

LEA r32/64,m的latency是1,throughput是1/2;

整體上LEA是要快一些的。

但這裡只是考慮到調用者的情況,如果是傳引用或者指針,那麼意味著被調用函數的內部需要做地址轉換,才能獲得實際的參數的值。

對於32位系統來說,參數是在棧上的,要先通過ebp獲得參數值(指針或者引用),再通過參數值獲得實際的指向或者引用的值,要進行兩次取地址的操作。

對於64位系統來說,參數是通過寄存器來傳遞的,仍然需要一次取地址操作才能獲得實際的值。

而如果是值傳遞的話,就沒那麼複雜了,參數就在棧上甚至在寄存器里,直接用就可以。因此整體來看,如果按值傳遞,被調用者的開銷確實更小。

所以,大致的結論是:對於內置類型來說,值傳遞確實比指針(和引用)效率要高

以上只是不開優化的結論,開優化以後,性能就不好說了,而且代碼的複雜度也會影響優化的效果。

雖然結論是值傳遞更快,但我仍然覺得如果沒有特別的需求,寫代碼的人不太建議關注它。

-------------------------

更進一步的思考一下:用值傳遞,那麼肯定是不關注這個參數在被調函數內的變化,而用指針或者引用,那麼一定是關注這種變化,兩種方式的用途是不一樣的,效率上有差異是必然的,但多數情況下,不需要人為的去優化,編譯器的優化已經足夠用了。


有一種很常見的認識,就是認為函數參數傳遞過程中,傳地址和傳引用是同樣快的。多數情況確實如此。然而:

如果在傳遞中有子類向父類退化的情況,並且會導致地址變化時,

在某些編譯器上傳遞指針的開銷比引用大,因為編譯器需要保證子類空指針轉換成父類指針時還是空指針,從而不得不先判斷指針是否為空,如果為空則傳0否則加一個偏移量後傳遞。而引用,在語義上不應該存在「空引用」,所以部分編譯器在傳遞引用時可以不執行這個判斷而直接傳遞加偏移量後的結果。

可以看以下代碼示例:

struct A{
int a;
};
struct B{
int b;
};
struct C : public A, public B{
int c;
};
extern C cref;
extern C *cp;
extern void testref(B);
extern void testp(B*);
void t1(){
testref(cref);
}
void t2(){
testp(cp);
}

部分編譯器上的反彙編結果如下

http://coliru.stacked-crooked.com/a/879f72b9ff928cf7 這裡可以看到gcc會進行優化。節選彙編結果如下

.LFB0:
.cfi_startproc
movq cref(%rip), %rax
leaq 4(%rax), %rdi
jmp _Z7testrefR1B
.cfi_endproc
.LFB1:
.cfi_startproc
movq cp(%rip), %rax
leaq 4(%rax), %rdi
testq %rax, %rax
movl $0, %eax
cmove %rax, %rdi
jmp _Z5testpP1B
.cfi_endproc

為了防止被牆,多給幾個鏈接

https://godbolt.org/g/GRrWc4 clang會執行這個優化。

https://godbolt.org/g/vMbJGw gcc 也會執行這個優化。

https://godbolt.org/g/BWMC5x 這個我不認識的ellcc編譯器也會執行這個優化。

https://godbolt.org/g/Be6QUc 也不認識的zapcc編譯器也會執行這個優化。

https://godbolt.org/g/cHEjAV VC++我使用我已知的優化選項都不足以產生這個優化。。不排除VC++一開始就沒有考慮這個優化,後來就不敢改了(擔心有人依賴這個特性亂用空引用)。

https://godbolt.org/g/k5v3He Intel C++編譯器我也沒法讓他產生這個優化,估計是為了兼容VC所帶來的副作用。

附:

  1. 如果你認為「空引用」這種C++語法上並不非法的東西有他的合理性從而認為這是VC和ICC不進行優化的原因,可以看看下面對dynamic_cast的介紹,,寧可拋異常也不返回空引用。可見空引用之病態。。

dynamic_cast 轉換 - cppreference.com

這裡還有一個this默認非null的優化,其實也是類似:this指針,變數的地址,無論如何都不應該是0。匿名用戶:GCC5以後的版本在編譯優化上有哪些進展?

2. 可以激發上述行為差異的除了多繼承,虛繼承,還有一個一般很少注意到的情況:父類A沒有虛函數而單繼承他的子類B有。此時子類對象虛表指針在對象頭部,而父類數據部分則在虛表後面(我查到的gcc和VC2個主流編譯器都是這個二進位結構,否則B的多繼承子類的對象布局會很麻煩),看起來就像子類B的第一父類是個虛表指針,而第二父類才是A,當然轉換成A時就可以產生地址偏移了。


參數裡面指針和引用是一樣的


根據具體需求決定使用按值傳遞和按引用/指針傳遞。不要去考慮哪個快哪個慢,能把傳參用出性能瓶頸也是奇葩了。


傳值要複製,指針和引用本質都是丟個地址


機器這麼牛逼了討論這個問題很無聊


int x= 1;
int p =x; //起個別名叫p
p = 2; //p就是x printf("%d
",x);

43: int x= 1;
00401058 mov dword ptr [ebp-4],1
44: int p =x; //起個別名叫p 0040105F
lea eax,[ebp-4]
00401062 mov dword ptr [ebp-8],eax
45: p = 2; //p就是x 00401065
mov ecx,dword ptr [ebp-8]
00401068 mov dword ptr [ecx],2
46: printf("%d
",x);
0040106E mov edx,dword ptr [ebp-4]
00401071 push edx
00401072 push offset string "%d
" (0042501c)
00401077 call printf (00403880)
0040107C add esp,8

//測試 吧上述代碼中的引用類型改為指針。 int x= 1;
int* p =x; //起個別名叫p
*p = 2; //p就是x printf("%d
",x);
//結果發現生成的反彙編結果一模一樣 //這裡我們暫時得出結論,,引用類型就是指針。或者說在底層實現上 就是指針


推薦閱讀:

TAG:C編程語言 | C | CC | CPrimer | C編程 |