直接上代碼:
void combine5(double data[],int length)
{
double sum = 0.0;
for(int i=0;i<length;i++)
{
sum *= data[i];
}
cout<<sum<<endl;
}
void combine6(double data[],int length)
{
double sum = 0.0;
int limit = length-1;
int i;
for(i=0;i<limit;i+=2)
{
sum = sum*data[i]*data[i+1];
}
for(;i<length;i++)
{
sum *= data[i];
}
cout<<sum<<endl;
}
void combine7(double data[],int length)
{
double sum1=0.0,sum2=0.0;
int limit = length-1;
int i;
for(i=0;i<limit;i+=2)
{
sum1 *= data[i]; // 合並下標為偶數的值, 0按偶數算
sum2 *= data[i+1]; // 合並下標為奇數的值
}
double sum = sum1*sum2;
for(;i<length;i++)
{
sum *= data[i];
}
cout<<sum<<endl;
}
上面三個函數的功能是一樣的,但是究其運行速度來說則不可同日語。為什么呢?
由於加法器和乘法器是完全流水線化的,這代表着他們可以一個時鍾周期執行多條指令,參見我前面寫的那一篇博文(優化的小細節)。減小代碼之間的相關性,增加代碼的並行性,可以大幅度增加代碼的執行效率,做到把CPU的能力都逼出來的地步。
combine5只是做了一些簡單的優化,combine6進行了循環展開,combine7既循環展開又多路並行。如果combine5運行時間是5,combine6的運行時間就會是2.5! combine7的運行時間就會是1!沒錯就是這么霸道!IA32以后的指令集都支持如此優化,但是絕大多數編譯器都不會幫你把代碼改成這個樣子,所以自己寫是最好的了。如果加上SSE的話,會把CPU的能力榨干的=。= 嘎嘎 下面來說說原因,為什么循環展開多路並行就比隨便寫快呢? 首先解釋一個名詞,叫關鍵路徑,循環的效率主要取決於關鍵路徑上的指令,循環的關鍵路徑可以看成是貫穿整個循環的變量與計算,減少關鍵路徑上的東西就等於加快了時間!那么循環展開就可以看成是減少了CPU走關鍵路徑的次數,但是每次關鍵路徑上的計算sum = sum*data[i]*data[i+1] 的相關性較大,兩次乘法無法並行,因為第二次乘法必須等第一次乘法完畢才能執行。多路並行就可以解決這個問題,sum1 *= data[i]; sum2 *= data[i+1];這兩個乘法互不依賴,完全可以並行,相當於只做了一次乘法,所以速度便大大加快了。
從上面的分析會發現,循環展開三次,三路並行會比上面的代碼更快,以此類推,代碼的優化效率必然會趨近於一個極限值。這個極限值就是CPU的吞吐量界限了,理論上講就無法再優化了。