++i? i++? i+=1? i=i+1? 何必糾結?

前言

今天在牛客上看面經,看到一個問題: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 的幾個時鐘週期,真的不值得。窗外的月光,更令人着迷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章