在前一篇博客里,我們提出了三種常見的循環優化的方法,主要包括:減少不必要的計算,減少不必要的函數調用,減少不必要的內存訪問。這三種方法跟特定的機器的特性無關,具有很強的通用性。今天,我們本着榨干機器最后一滴性能的原則,再進一步提出幾種跟指令執行有關系的優化方法。
一.循環展開
循環展開可以減少循環的次數,對程序的性能帶了兩方面的提高。一是減少了對循環沒有直接貢獻的計算,比如循環計數變量的計算,分支跳轉指令的執行等。二是提供了進一步利用機器特性進行的優化的機會。
例子:
優化前的代碼見前一篇博客里的sum3.
優化后:
void sum4(vec_ptr v,data_t *dest){ int i; int len=vec_length(v); int limit=len-3; data_t *data=get_vec_start(v); data_t acc=0; for(i=0;i<limit;i+=4){ acc=acc+data[i]+data[i+1]; acc=acc+data[i+2]+data[i+3]; } for(;i<len;++i) acc+=data[i]; *dest=acc; }
通過循環展開,每次迭代將累加4個元素,減少了循環次數,從而減少了總的執行時間(單獨使用這種優化方法,對浮點數累乘幾乎沒有提高,但是整數累乘得益於編譯器的重關聯代碼變化會有大幅度提高)。
這種優化可以直接利用編譯器完成,將優化level設定到較高,編譯器會自動進行循環展開。使用gcc,可以顯式使用-funroll-loops選項。
二.提高並行性
現代處理器大多采用了流水線、超標量等技術,可以實現指令級並行。我們可以利用這個特性對代碼做進一步的優化。
2.1使用多個累積變量
優化代碼示例
void sum5(vec_ptr v,data_t *dest){ int i; int len=vec_length(v); int limit=len-1; data_t *data=get_vec_start(v); data_t acc0=0; data_t acc1=0; for(i=0;i<limit;i+=2){ acc0+=data[i]; acc1+=data[i+1]; } for(;i<len;++i) acc0+=data[i]; *dest=acc0+acc1; }
這里同時使用了循環展開和使用多個累加變量,一方面減少了循環次數,另一方面指令級並行的特性使得每次迭代的兩次加法可以並行執行。基於這兩點可以顯著減少程序執行的時間。通過增加展開的次數和累加變量的個數,可以進一步提高程序的性能,直到機器指令執行的吞吐量的極限。
2.2重結合變換
除了使用多個累積變量顯式利用機器的指令級並行特性外,還可以對運算重新結合變換,打破順序相關性來享受指令級並行帶來的好處。
在sum4中,acc=acc+data[i]+data[i+1]的結合順序是acc=(acc+data[i])+data[i+1];
我們將之變成acc=acc+(data[i]+data[i+1]);
代碼如下:
void sum6(vec_ptr v,data_t *dest){ int i; int len=vec_length(v); int limit=len-3; data_t *data=get_vec_start(v); data_t acc=0; for(i=0;i<limit;i+=4){ acc=acc+(data[i]+data[i+1]); acc=acc+(data[i+2]+data[i+3]); } for(;i<len;++i) acc+=data[i]; *dest=acc; }
進一步增加循環展開的次數,可以進一步提高程序性能,最終也可以達到機器指令執行的吞吐量的極限。(在循環展示提到的整數乘法的性能提高就在於編譯器隱式采取了這種變換,但是由於浮點數不具備結合性,所以編譯器沒有采用,但是程序員在保證程序結果正確性的情況下,可以顯式使用這一點)。
根據相關資料的數據,sum5進行5路展開,5路並行可以達到sum1近10倍的性能。
除了這兩篇博客提到的方法,還可以使用SIMD指令對程序加速。
參考文獻:CSAPP 2nd edition
