最佳化 C++/程式碼最佳化/流水線
條件跳轉機器語言指令(也稱為 分支)可能由許多 C++ 語言特性生成,其中包括 if-else、for、while、do-while 和 switch-case 語句,以及布林和條件表示式運算子。
現代處理器僅在能夠預測分支的情況下才能有效地處理分支。如果預測錯誤,則 流水線 在後續指令上已完成的步驟將無用,處理器必須從分支目標指令重新開始。
分支預測基於對同一指令的先前迭代。如果分支遵循規律模式,則預測會成功。
最佳情況是分支指令始終具有相同的效果;在這種情況下,預測幾乎總是正確的。最糟糕的情況是分支指令的結果模式與分支預測行為相反。由於預測器通常假設結果會重複出現,因此這將是一個結果始終與先前結果相反的分支。在這種情況下,預測永遠不正確,流水線始終被中斷。並非所有處理器都使用這種預測行為,一些簡單的處理器始終假設不會發生跳轉。分支指令具有隨機結果的情況會導致預測平均情況下只有一半時間是正確的。但是,隨機分佈意味著實際結果將從始終正確到始終錯誤,呈高斯形狀分佈。
在瓶頸中,應避免難以預測的分支。如果分支預測非常糟糕,即使用相當慢的指令序列替換它也可能導致速度提升。
在本節中,將介紹一些用等效指令替換分支的技術。
如果您需要檢查一個整數 i 是否在兩個整數 min_i 和 max_i(包括)之間,並且您確定 min_i <= max_i,請使用以下表達式
unsigned(i – min_i) <= unsigned(max_i – min_i)
在給定的條件下,上述公式等效於以下更直觀的公式
min_i <= i && i <= max_i
前一個公式執行兩次差值和一次比較,而後一個公式執行零次差值和兩次比較。對於流水線處理器,比較比差值慢,因為它們意味著分支。 [存疑 ]
此外,如果 min_i 是一個值為零的常量表達式,則兩個差值會消失。
特別是,要檢查一個整數 i 是否是 size 個元素陣列的有效索引,即執行陣列邊界檢查,請使用以下表達式
unsigned(i) < unsigned(size)
顯然,如果表示式已經是 unsigned 型別,則不需要轉換。
不要使用條件表示式,在其中兩種情況都是常量,而是使用一個有兩位置的查詢表。
如果您有以下語句,其中 c 和 d 代表常量表達式,b 代表布林表示式
a = b ? c : d;
這等效於以下程式碼
if (b) a = c;
else a = d;
嘗試用以下程式碼替換它,它等效但可能更快
static const type lookup_table[] = { d, c };
a = lookup_table[b];
條件表示式被編譯成分支。如果此類分支未被很好地預測,它將比查詢表花費更長時間。
此技術也可以應用於一系列條件表示式。例如,不要使用以下程式碼
a = b1 ? c : b2 ? d : b3 ? e : f;
這等效於以下程式碼
if (b1) a = c;
else if (b2) a = d;
else if (b3) a = e;
else a = f;
嘗試檢視以下程式碼是否更快
static const type lookup_table[] = { f, e, d, d, c, c, c, c };
a = lookup_table[b1 * 4 + b2 * 2 + b3];
嘗試在您需要訪問引用物件之前稍微計算一下指標或迭代器的值。
例如,在迴圈中,以下語句
a = *++p;
可能比以下語句效率略低
a = *p++;
在第一種情況下,指標或迭代器的值在用於訪問引用物件之前才計算,而在第二種情況下,它是在前一次迭代中計算的。在流水線處理器中,在第二種情況下,指標的增量可以與訪問引用物件同時執行,而在第一種情況下,這兩個操作必須序列化。