首先,上一篇文章討論cache missing的重要性時,用了list做比較,目的並不是說list沒有用,而是說明cache missing會對性能有重要影響。如果元素不多,並且對象復制的代價很大,那麼list可能就是更好的選擇。其次,這裡討論的大部分是編碼時一些比較底層的技巧,當遇到性能問題時,應該先考慮是否能在高層改進算法,減少運算,實在不行,在考慮這類優化技巧。性能優化的挑戰就在於沒有完美的永遠適用的方案,了解這些技巧讓我們在優化代碼時有更多武器,但最終選擇哪個方案還需要更加實際情況,並且以profile的實際數據為依據來做。 1. Data Layout 調整數據布局是常見的優化手段,做此類優化時有幾點需要注意:首先是內存占用,現代編譯器默認大多以16或32位對齊,因此 復制代碼 struct BadLayout { int8_t i0; int32_t i1; int8_t i2; }; struct GoodLayout { int8_t i0; int8_t i2; int32_t i1; }; 復制代碼 sizeof(Goodlayout) >= sizeof(BadLayout) 在vs2013默認對齊設置下,BadLayout==12 byte,GoodLayout==8byte。 其次,經常訪問或者相關的數據應該放到一起,減少cache missing。Going native2013 Andrei Alexandrescu介紹了facebook做的重要性能優化就是把php代碼編譯為c++代碼,而在代碼轉換中重要的一步就是根據數據的”hotness”重新布局。 復制代碼 struct BadLayout { auto user0_data0; auto user1_data1; auto user0_data1; auto user1_data0; }; struct GoodLayout { auto user0_data0; auto user0_data1; auto user1_data0; auto user1_data1; }; 復制代碼 最後,可維護性!這一點非常重要,對於一些生命周期較長的項目來說,把數據按邏輯組織更易於維護,減少潛在bug的重要性,如果性能差別不大,我通常更願意讓代碼看起來好讀J 2. Code cache 復制代碼 struct BitBool { bool b0 :1; bool b1 :1; bool b2 :1; } struct NormalBool { bool b0; bool b1; bool b2; } 復制代碼 上次的例子中,這段代碼比較有爭議,讓我們從時間和空間方面來分析。時間上,因為BitBool需要額外指令來訪問元素,因此效率一定比NormalBool低,但差別非常小,幾乎可以忽略。再看空間上,但從結構本身看,顯然BitBool更小,但是由於訪問元素需要額外指令,實際應用中,生成的代碼一定比NormalBool多,讀取訪問的次數越多,生成的代碼也越多(內聯的結果),而代碼也需要占用內存空間!!cache line中通常一半是代碼,一半是數據。因此,不一定因為BitBool本身小就得到更好的cache。大部分文章在討論cache missing時都只介紹了數據,而忽略了代碼也需要占用內存,也會有cache missing。某些游戲引擎會在update entity時先把對象按照類型排序,就是為了減少代碼的cache missing。 最後,這個例子的目的是讓大家了解過度優化可能並不會帶來性能提升,實際應用中兩種寫法的雖然有性能差距,但基本可以忽略。 3. more about bit field 上一個例子讓我想起了bitfield另一個微妙的地方,假設f1和f2在兩個不同線程中,考慮下面代碼是安全的嗎? 復制代碼 struct BitField { bool b0 : 1; bool b1 : 1; bool b2 : 1; uint8_t i0 :3; } BitField bf; std::mutex mtx1; std::mutex mtx2; //thread 1 void f1() { mtx1.lock(); bf.b1 = somevalue; mtx1.unlock(); } //thread 2 void f2() { mtx2.lock(); bf.i0 = somevalue; mtx2.unlock(); } 復制代碼 No!!!雖然代碼可以通過編譯運行,但卻並不是線程安全的,因為b1,i0都屬於同一快內存”單元”,因此根本無法生成只更新b1,但是不寫入i0的代碼!!實際上c++11明確指出了這種情況會導致race,臨近的bit總是被當做一個”對象” :)