在C語言中,分支預測是一種關鍵的性能優化策略,旨在減少由於分支語句(如if、switch等)導致的預測錯誤,從而提高程序的執行效率。本文將通過一個經典案例來演示如何使用分支預測來優化程序性能,並對優化前後的性能進行對比。
原始代碼
如果讓你用C語言寫一個函數,用來合並兩個已經有序的數組為一個有序數組,升序。
可能很多朋友都會寫成大概下面這個樣子代碼:
#include <stdio.h>
#include <sys/time.h>
void mergeSortedArrays(int *arr1, int size1, int *arr2, int size2, int *result) {
// 比較兩個數組的元素並將它們合並到結果數組中
while (size1 > 0 && size2 > 0) {
if (*arr1 <= *arr2) {
*result = *arr1 ;
size1--;
} else {
*result = *arr2 ;
size2--;
}
}
// 如果arr1中還有剩餘元素,將它們復制到結果數組中
while (size1 > 0) {
*result = *arr1 ;
size1--;
}
// 如果arr2中還有剩餘元素,將它們復制到結果數組中
while (size2 > 0) {
*result = *arr2 ;
size2--;
}
}
int main() {
struct timeval start, end;
int i=0;
int arr1[] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29};
int size1 = sizeof(arr1) / sizeof(arr1[0]);
int arr2[] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30};
int size2 = sizeof(arr2) / sizeof(arr2[0]);
int mergedSize = size1 size2;
int result[mergedSize];
// 記錄函數執行前的時間
gettimeofday(&start, NULL);
// 合並兩個已排序數組
mergeSortedArrays(arr1, size1, arr2, size2, result);
// 記錄函數執行後的時間
gettimeofday(&end, NULL);
// 計算函數執行時間(以微秒為單位)
long seconds = end.tv_sec - start.tv_sec;
long micro_seconds = end.tv_usec - start.tv_usec;
double execution_time = seconds micro_seconds / 1e6;
// 打印執行時間
printf("函數執行時間: %f 秒\n", execution_time);
// 輸出合並且有序的數組
printf("合並且有序的數組: ");
for (i = 0; i < mergedSize; i )
printf("%d ", result[i]);
printf("\n");
return 0;
}
主函數裡面構建兩個人有序數組arr1和arr2,然後重點是使用mergeSortedArrays函數來合並兩個數組,然後在合並函數的前後分別調用gettimeofday來計算合並函數的耗時,最後打印出合並函數耗時和合並後的數組結果。非常簡單,重點是如下的合並函數mergeSortedArrays的代碼實現。
![](https://news.xinpengboligang.com/upload/keji/74c26d7eead06d7cab694c2f8a1a66c1.jpeg)
合並函數代碼
下面我們來看看具體合並實現。函數將2個被合並有序數組首地址和長度傳進來,另外傳進來一個保存最終合並後數組數據的數組首地址result。
1.函數中第一個while循環中,用size1和size2來記錄兩個被合並數組的剩餘長度。隻要兩個數組中有一個遍歷完了就退出循環。循環裡面用分支語句if來逐個去比較兩個數組成員,哪個成員的值較小就把他復制到result結果數組裡去。
2.第二個while循環主要是看arr1數組中是否還有剩餘元素,如果有就逐個復制到結果數組中。
3.第三個while循環主要是看arr2數組中是否還有剩餘元素,如果有就逐個復制到結果數組中。
講完了,很多朋友是不是覺得代碼非常精簡漂亮,效率很高啊,沒問題,非常完美!
好那就運行一下吧,在ubuntu下面使用gcc並且加了-O2優化選型,優化代碼,編譯運行,結果如下:
原始代碼運行結果
可以看到合並函數耗時是0.000002s,也就是2微秒,非常短,不錯。
但是看看代碼真的沒辦法優化這個代碼了嗎?如果我們來結合分支預測來考慮一下呢?
分支預測
我們來看一下整個函數中總共有4處條件判斷。我們分別來看看四個分支條件的可預測性分別怎麼樣?
![](https://news.xinpengboligang.com/upload/keji/07ee5e0b05d396350208ba305e2c0f1e.jpeg)
4個判斷分支預測
第一處,也就是最後一個while條件判斷,因為大多數情況下條件都會返回true,最後一次返回false,所以這個是可以預測的。
第二處,和第一處是一樣的,也是可預測的。
第三處,是if判斷,條件式判斷兩個數組的兩個元素大小,這個大概一半是返回true,一半概率返回false,所以是不可預測的。
第四處,有事while條件判斷,判斷是否有至少有一個數組成員被復制完,這個大多數情況下是返回true的,所以也是可預測的。
總的結果如下表:
![](https://news.xinpengboligang.com/upload/keji/a8874ec6b3d21a25e51a3b63a4c78d05.jpeg)
可預測否?
優化思路
那麼既然第三處的if分支預測是不可預測的我們來否做點什麼來優化一下?
我們知道,分支預測的目的是在程序執行時預測分支語句(如if語句、循環中的條件判斷等)的走向,以便更好地執行預測分支的代碼路徑。
分支預測失敗率表示在分支語句的預測中,實際執行的路徑與預測的路徑不一致的比例。高分支預測失敗率表示預測不準確,導致程序執行了不必要的分支,從而浪費了處理器的時間。
一個優秀的分支預測器應該能夠高效地識別程序中的分支模式,並根據歷史執行情況進行準確的預測。如果分支預測準確性良好,那麼程序能夠更好地利用指令流水線和其他硬件特性,提高執行效率。相反,低分支預測準確性可能導致流水線中的指令冒險(pipeline stalls)和性能下降。
那麼既然我們第三處這個if判斷既然大半概率是會失敗,那麼我們是不是就不要用if分支語句來寫這個代碼,我們可以用位操作來實現這個if else分支代碼,避免分支預測失敗,從而優化代碼效率。
優化代碼
下面來看看如何用位操作代碼代替ifelse分支功能吧。其它代碼都不改,隻是改一下合並函數第一個while循環裡面代碼:
void mergeSortedArrays(int *arr1, int size1, int *arr2, int size2, int *result) {
// 比較兩個數組的元素並將它們合並到結果數組中
while (size1 > 0 && size2 > 0) {
int cmp = (*arr1 <= *arr2);
int min = *arr2 ^ ((*arr2 ^ *arr1) & (-cmp));
*result = min;
arr1 = cmp;
size1 -= cmp;
arr2 = !cmp;
size2 -= !cmp;
}
// 如果arr1中還有剩餘元素,將它們復制到結果數組中
while (size1 > 0) {
*result = *arr1 ;
size1--;
}
// 如果arr2中還有剩餘元素,將它們復制到結果數組中
while (size2 > 0) {
*result = *arr2 ;
size2--;
}
}
![](https://news.xinpengboligang.com/upload/keji/26a638fab860bf05a35be84700d9f4a5.jpeg)
新代碼
重點就是上圖紅框中的代碼,首先用cmp來保存兩個數組中元素的大小結果,要門是0,要門是1。接著借用異或(^)這個位操作巧妙拿到兩個數中較小得的賦給min變量保存。我們來分析一下如何拿到兩個數中較小的數的。
如果arr1小於arr2,那麼cmp就是1,-cmp就是-1,-1其實就是補碼形式存儲,所有位全是1,-cmp與上arr1^arr2,那就是還是arr1^arr2,arr1^arr2其實就是兩個數隻要哪一位不同都會被置1,相當於不同位掩碼。然後這個掩碼再異或arr2其實就是將arr2中和arr1不同的位全部逆反。這樣arr2是不是就變成arr1一樣,所以min其實拿到就是arr1值。
如果arr1大於arr2,那麼cmp是0,-cmp也是0,arr2異或0還是arr2。
這段代碼最復雜部分就講完了。
性能對比
下面我們來運行一下優化後代碼,同上,還是在ubuntu下面使用gcc並且加了-O2優化選型,優化代碼,編譯運行,結果如下:
優化代碼運行結果
可以看到合並函數耗時是0.000001秒,也就是1微秒,效率提升原來2倍。
需要指出的是,有些編譯器自動優化後,效果不一定有提升。
提升這麼點時間,有意義嗎?真沒有意義嗎?如果說這個被合並的數組將來數據量非常大,比如說數組長度是1000000,那就是合並時間就可以縮短1秒了,那就非常厲害了。現在機器學習、大數據時代,數據大是很正常的,能縮短數據處理時間是很有價值的。
後續持續更新系列高質量文章,碼字不易,覺得寫的不錯歡迎關註、點贊、收藏以及提問。