C++之那些年踩過的坑

茶花盛開發表於2017-04-22

上次提到了一個問題:程式碼優化。並留了一個小測試:無符號數與有符號數的效能比較。不知道有沒有人去實驗。我做了一個簡單的測試:

C++之那些年踩過的坑

#include <iostream>#include <chrono>int main()

C++之那些年踩過的坑

在上述程式碼在 VS 的 Debug 模式下執行(Release就優化掉了),穩定後執行時間在 2800ns,然後把註釋去掉,再次執行,穩定後執行時間還是 2800ns。在我在電腦上,計算有符號型別和無符號型別幾乎是沒有差別的,我相信在絕大多數的電腦上也是相同的結果。

類似的還有浮點數的計算比整型數慢

首先我要宣告:我不是反對程式碼優化。對於有很多流傳廣泛的所謂的優化技巧,我覺得我們應該應該抱著學習探索的心態,而不是一味地追求一些沒有什麼意義的東西。有些優化,確實很精妙。但很多所謂的技巧,看起來的意思就是:做編譯器的那群人都是傻逼。想優化我們的程式,這是正常的、應該的想法,但我們應該用科學的方法,而不是聽了一些奇淫技巧,卻不知道里面發生了什麼。

其實很簡單,探究效能瓶頸靠 profiling,探究程式碼背後的不為人知的故事看 assembly。我們先講後面一個。如果你想學習C/C++可以來這個群,首先是三三零,中間是八五九,最後是七六六,裡面有大量的學習資料可以下載。

我想大多數人還是在 Windows 下程式設計,所以用的肯定也是宇宙最強 IDE VS。在 VS 下看彙編很簡單,隨便設定一個斷點,然後按除錯下面的開始除錯,然後在開啟在除錯下的視窗找到反彙編,就可以看了。比如我們研究有符號數和無符號數,先寫一個程式:

int main()

然後按照剛剛的方法(在 Debug 下)看反彙編:

C++之那些年踩過的坑

/*unsigned */int a = 123456789;00B4104E mov dword ptr [a],75BCD15h

C++之那些年踩過的坑

然後在把註釋去掉試試:

C++之那些年踩過的坑

unsigned int a = 123456789;00C3104E mov dword ptr [a],75BCD15h

C++之那些年踩過的坑

幾乎是一模一樣的,最大的差別就是有符號數使用 idiv指令(帶符號除法),無符號數使用 div指令(不帶符號除法),而這兩種指令,CPU週期都是一樣的。

當然我不是說不用無符號數,而是說我們用什麼要看場合,而不是你覺得用了效能更好,除非是被大眾認可的或者你經過嚴謹的測試的。像對於某些書籍或者什麼地方說,只要確定範圍不為負數的,就用無符號型別,我是不認可的。如果你講範圍,那如果一個有符號型別不夠用,那麼通常(相同bit下)它對應的無符號型別也不夠用。比如你 int32_t 不夠用,就應該用 int64_t,如果還不夠,考慮寫個 BigInteger類 吧 ? 。不過對於無符號和有符號型別,它們之間的效能在當代確實是幾乎沒有什麼差別。那具體什麼場合用什麼呢?這個也不一定,比如一般來說:

  • 對於位儲存、位運算、模運算等,使用無符號型別

  • 對於一般運算使用有符號型別

其實我對於無符號有符號是覺得很那個什麼的。。平時我們說一個數,要麼就是整數,要麼就是小數得了,還要去分有沒有符號那真的是。不過因為是 C++ 所以也就沒什麼奇怪的了。

好像有點偏離我想說的主題了(拽回來

我並不是想專門講這個什麼有符號無符號,而是想借這個題引出(我的)一些看法:

  • 過早的優化是萬惡之源

  • 不要試圖幫編譯器優化

  • 優化時不要去猜測,想當然得去優化自己“覺得”效能差的地方

  • 探究效能的瓶頸靠 profiling

我們一點一點講。

對於一個需求,我們應該先完成功能,若效能達不到要求之後,在確定瓶頸之後再去優化。過早優化,不僅讓程式碼不直接,還容易出bug,還可能對效能幾乎沒有影響。而且,我們優化時,應該關注大方向,確定大方向是正確的。比如寫一個演算法,我們首先應該確保 Big-O 的時間複雜度能達標,可以用 O(n) 的就不用 O(nlogn),可以用 O(nlogn) 的就不用 O(n²),而不是先在那裡扣 i++ 還是 ++i。另外,不要想著去幫編譯器優化,因為編譯器是一堆比你強不知道多少的人寫出來的,而對於一般人,想著去幫編譯器優化,大部分是無效的,少部分是錯的。比如有人學了一點點 std::move,就老是想著 move move move 去提高效能,舉個例子,(不同的)容易寫出這樣的程式碼:

template <typename T>T create()

確實執行不會錯,但是,程式碼背後做的事情不一定就跟你想的一樣,往往跟你想象的還不一樣。有些情況編譯器可以採用更好的辦法,結果因為你那麼一搞,迫不得已只能採用次一點的辦法。可以看看 這個問題 ,不贅述了。

還有比如說用異或來交換兩個變數,有人就會想,用位運算,不僅不需要建立臨時變數,而且位運算一般不是更快嘛!

如果你已經看了上面的連結,那麼你也就知道了,你(幾乎)不會知道編譯器做了什麼,編譯器可以做的優化超出你的想象(不過有的時候人能明顯看出來的優化編譯器卻做不到,但影響不大),在我的系列文章(二)中也反覆強調了,不要試圖幫助編譯器去優化。若你想探究一小段程式碼背後的細節,就去看反彙編吧!


相關文章