前言
今天在牛客上看面經,看到一個問題:num++; num+=1; num = num +1; 哪個效率最高?
自從學習C語言開始,我就在糾結for語言應該寫i++,還是++i,其實這個問題,可以通過彙編代碼來看看。
區別
首先說明,自增操作符是 num = num + 1 或者 num += 1 的縮寫,但又有不同,比如 C++ 中涉及到了操作符重載,其他語言又有不同的特性,但是本文只討論最簡單最經典的 C 。
賦值順序:
int m = i++; // 變量 m 被賦值爲 i 後,變量 i 才自增
int m = ++i; // 變量 i 自增後,變量 m 才被賦值爲 i
i++ 只能作爲右值,而 ++i 可以作爲左右值:
int *p1 = &(++i); // 正確
int *p2 = &(i++); // 錯誤
++i = 1; // 正確
i++ = 1; // 錯誤
i++ 不能作爲左值的原因,觀察其彙編可以知道,i++ 返回的只是一個臨時變量,或者說只是一個存在寄存器中的值。而 ++i 返回的就是 i 本身,或者說是 i 的引用地址。
底層彙編
先來看一段代碼:
int main() {
int i = 0;
i++;
++i;
i+=1;
i=i+1;
return 0;
}
在 gcc -O0 無優化編譯後的彙編代碼爲:
a.out`main:
0x100000f70 <+0>: pushq %rbp
0x100000f71 <+1>: movq %rsp, %rbp
0x100000f74 <+4>: xorl %eax, %eax
0x100000f76 <+6>: movl $0x0, -0x4(%rbp)
0x100000f7d <+13>: movl $0x0, -0x8(%rbp)
0x100000f84 <+20>: movl -0x8(%rbp), %ecx
0x100000f87 <+23>: addl $0x1, %ecx
0x100000f8a <+26>: movl %ecx, -0x8(%rbp)
0x100000f8d <+29>: movl -0x8(%rbp), %ecx
0x100000f90 <+32>: addl $0x1, %ecx
0x100000f93 <+35>: movl %ecx, -0x8(%rbp)
0x100000f96 <+38>: movl -0x8(%rbp), %ecx
0x100000f99 <+41>: addl $0x1, %ecx
0x100000f9c <+44>: movl %ecx, -0x8(%rbp)
0x100000f9f <+47>: movl -0x8(%rbp), %ecx
0x100000fa2 <+50>: addl $0x1, %ecx
0x100000fa5 <+53>: movl %ecx, -0x8(%rbp)
0x100000fa8 <+56>: popq %rbp
0x100000fa9 <+57>: retq
可以驚訝地發現,四種寫法的彙編代碼竟然都一樣:
movl -0x8(%rbp), %ecx
addl $0x1, %ecx
movl %ecx, -0x8(%rbp)
從這一點看,似乎四種寫法的開銷都是兩次內存訪問。但是他們的功能不都一樣,我們可以這樣再改:
int main() {
int i = 0;
int m;
m = i++;
m = ++i;
m = i+=1;
m = i=i+1;
return 0;
}
再看彙編,發現了變化:
a.out`main:
0x100000f70 <+0>: pushq %rbp
0x100000f71 <+1>: movq %rsp, %rbp
0x100000f74 <+4>: xorl %eax, %eax
0x100000f76 <+6>: movl $0x0, -0x4(%rbp)
0x100000f7d <+13>: movl $0x0, -0x8(%rbp)
0x100000f84 <+20>: movl -0x8(%rbp), %ecx
0x100000f87 <+23>: movl %ecx, %edx
0x100000f89 <+25>: addl $0x1, %edx
0x100000f8c <+28>: movl %edx, -0x8(%rbp)
0x100000f8f <+31>: movl %ecx, -0xc(%rbp)
0x100000f92 <+34>: movl -0x8(%rbp), %ecx
0x100000f95 <+37>: addl $0x1, %ecx
0x100000f98 <+40>: movl %ecx, -0x8(%rbp)
0x100000f9b <+43>: movl %ecx, -0xc(%rbp)
0x100000f9e <+46>: movl -0x8(%rbp), %ecx
0x100000fa1 <+49>: addl $0x1, %ecx
0x100000fa4 <+52>: movl %ecx, -0x8(%rbp)
0x100000fa7 <+55>: movl %ecx, -0xc(%rbp)
0x100000faa <+58>: movl -0x8(%rbp), %ecx
0x100000fad <+61>: addl $0x1, %ecx
0x100000fb0 <+64>: movl %ecx, -0x8(%rbp)
0x100000fb3 <+67>: movl %ecx, -0xc(%rbp)
0x100000fb6 <+70>: popq %rbp
0x100000fb7 <+71>: retq
m = i++; 對應的彙編爲:
movl -0x8(%rbp), %ecx
movl %ecx, %edx
addl $0x1, %edx
movl %edx, -0x8(%rbp)
movl %ecx, -0xc(%rbp)
三次內存訪問,用了兩個寄存器。
另外三種寫法的彙編爲:
movl -0x8(%rbp), %ecx
addl $0x1, %ecx
movl %ecx, -0x8(%rbp)
movl %ecx, -0xc(%rbp)
同樣三次內存訪問,不過相比之下,只用了一個寄存器。
這麼一看,由於寄存器操作速度是相當快的,訪問內存纔是效率的決定因素,所以四種寫法效率差別並不大,甚至可以忽略不計。硬要說就是 i++; 這種寫法最慢,另外三種寫法一樣。
結論
既生 ++i ,何生 i++ ?唯一一個理由就是,手指有些短,習慣先按 i 再按 + 。
不過用生命中寶貴的幾秒鐘來糾結 CPU 的幾個時鐘週期,真的不值得。窗外的月光,更令人着迷。