C++ Low level performance optimize程序員
1. May I have 1 bit ?算法
下面兩段代碼,哪個佔用空間更少,那個速度更快?思考10秒再繼續往下看:)性能優化
//v1 struct BitBool { bool b0 :1; bool b1 :1; bool b2 :1; } BitBool bb; bb.b1 = true; //v2 struct NormalBool { bool b0; bool b1; bool b2; } NormalBool nb; nb.b1 = true;
第一種一般被認爲是優化的版本,甚至UE3裏都有不少相似代碼,但實際上卻在兩方面都不佔優,why?緣由是現代大部分cpu都沒有指令能直接訪問1bit數據,而多線程
bb.b1 = truedom
至關於函數
bb.b1 |= (1<<xxxx); 相應彙編代碼可能爲(實際彙編根據編譯器可能會不一樣):工具
shl cl,2
xor cl,al
and cl,4
xor al,cl性能
而測試
nb.b1 = true;只須要
mov BYTE PTR [rcx+2], dl優化
當考慮空間優化時通常只考慮到了數據佔用空間,而沒有考慮代碼所佔用的內存。所以雖然sizeof(BitBool)==1==8bit (默認8bit對齊的系統),但訪問b1的指令所佔空間卻須要8~11byte! 考慮到訪問成員的代碼一般會成爲內聯函數,所以BitBool所佔空間爲 1 + 11 * n ,n爲代碼中須要訪問數據的次數! 而NormalBool雖然須要3byte,但其訪問代碼只須要3byte機器碼,所佔空間爲3 + 3 * n。所以不管在性能仍是空間性,NormalBool均更好!
2 Cache missing is killer!!!
把一個隨機數列依次插入list和vector,保持兩個新數列從小到大排序:7,5,2,7,9,3 ===> 2,3,5,7,7,9 ,下面是代碼,對於不一樣的數據量n,哪種方法更好?
std::list<int> myList; std::vector<int> myVec; //create a random number array const int size = 50000; std::array<int, size> myArr; for(int i = 0; i < size; i++) { myArr[i] = rand() % 2000; } //pre-allocate memory myVec.reserve(size); myList.resize(size,65535); //fill vector for(int i = 0; i < size; i++) { int value = myArr[i]; auto it = myVec.begin(); for(; it != myVec.end(); it++) { if(*it > value) { it = myVec.insert(it, value); break; } } if(it == myVec.end()) myVec.push_back(value); } //fill list for(int i = 0; i < size; i++) { int value = myArr[i]; auto it = myList.begin(); for(; it != myList.end(); it++) { if(*it > value) { it = myList.insert(it, value); break; } } if(it == myList.end()) myList.push_back(value); }
兩段代碼幾乎相同從算法分析的角度看,頻繁插入操做是vector的災難,但實際測試結論是不管size爲多大,vector老是比list快,而且size越大,差距越明顯,在個人機器上當size=50k時快了近10倍!!why?首先list須要佔用更多內存,其次vector老是保證元素位於連續的內存,這是最重要的!Cache missing致使的性能損失甚至比複製元素還嚴重。對現代CPU來講,運算速度已經很是快,一次cache missing就會浪費n個cpu週期,合理組織數據,讓cpu減小等待時間是現代cpu很是重要的優化手段。
注意,上面的演示代碼只是爲了展現cache missing的重要性,並非完成這個任務的最優方法,另外實際狀況下對於複雜類型來講,隨着複製代價的提升,vector未必就能總勝出了:)。
3. False Sharing(cache-line ping-ponging)
大部分程序員都據說過cache missing,但知道false sharing的就不那麼多了。爲了討論false sharing,首先要介紹cache line. 就像CPU不能讀取1bit同樣,cpu訪問cache時一般也會多讀取一些額外數據,同時讀取的這一段數據就稱爲一條cache line。每一級cache都由n條cache line組成,對於intel i級的cpu來講cache line大小爲64byte。假設cpu須要訪問變量v時, v地址附近的數據都被讀入cache中。對於單核的世界來講,一切都很好,但對於多核心下的多線程設計來講,問題就來了,假設v被加載到了第一個核的cache中,此時另外一個核心須要訪問臨近v的變量v1怎麼辦? 若是都是隻讀操做,那麼每一個核心能夠各保存一份cache line v的副本,不會有衝突。但若是線程1須要修改v,線程2須要修改v2怎麼辦呢,顯然會致使不一樣核心的cache line狀態不一致。爲了解決這個問題,整個cache line都要被來回從新加載。好比:線程1從主內存加載cache line v,修改v,把整個cache line回寫到主內存,線程2再重複這個過程修改v1,這種狀況就稱爲false sharing,顯然若是在並行運行的核心代碼中出現這種的狀況,性能是很是糟糕的,而這樣的代碼一般又不太容易發現
下面是wiki上false sharing的一個例子:
struct foo { int x; int y; }; static struct foo f; int sum_a(void) { int s = 0; for (int i = 0; i < 1000000; ++i) s += f.x; return s; } void inc_b(void) { int i; for (i = 0; i < 1000000; ++i) ++f.y; }
假設sum_a和inc_b兩個函數同事運行在不一樣核心的不一樣線程上,f的全部成員都在同一條cache line,inc_b在不聽修改內存中的值,所以致使false sharing。
在作性能優化前,必定要先profile,profile,profile!!!不少狀況下,問題所在的位置和程序員所預期的都不同,盲目修改代碼甚至有可能下降程序性能!!!
ps:最悲劇的狀況就是沒有任何可靠的profile工具,還必須作性能優化,我目前的狀況就是這樣,怎一個慘字了得....
more refereence:
Modern C++: What You Need to Know
Native Code Performance on Modern CPUs: A Changing Landscape
Native Code Performance and Memory: The Elephant in the CPU