摘要:瞭解如何使用 Microsoft Visual C++ 2005(以前稱為 Visual C++“Whidbey”)的浮點語義管理方法來最佳化浮點代碼。建立快速程式,同時確保僅對浮點代碼執行安全最佳化。
C++ 中的浮點代碼最佳化
C++ 最佳化編譯器不僅能夠將原始碼轉換為機器碼,而且能夠對機器指令進行適當的排列以便改善性 能和/或減小大小。遺憾的是,許多常用的最佳化在應用於浮點計算時未必安全。在下面的求和演算法 [1] 中,可以看到這方面的一個恰當的樣本:
float KahanSum( const float A[], int n )
{
float sum=0, C=0, Y, T;
for (int i=0; i<n; i++)
{
Y = A[i] - C;
T = sum + Y;
C = T - sum - Y;
sum = T;
}
return sum;
}
該函數將數組向量 A 中的 n 個浮點值相加。在迴圈體中,演算法計算 一個“修正”值,然後將其應用於求和的下一步。與簡單的求和相比,該方法大大減小了累積性舍入 誤差,同時保持了 O(n) 時間複雜性。
一個不完善的 C++ 編譯器可能假設浮點演算法遵循與實數演算法相同的代數規則。這樣的編譯器可能 繼而錯誤地斷定
C = T - sum - Y ==> (sum+Y)-sum-Y ==> 0;
也就是說,C 得到的值總是常量零。如果隨後將該常量值傳播到後續運算式中,迴圈體將化簡為簡 單的求和。更準確地說,就是
Y = A[i] - C ==> Y = A[i]
T = sum + Y ==> T = sum + A[i]
sum = T ==> sum = sum + A[i]
因此,對於不完善的編譯器而言,KahanSum 函數的邏輯轉換將是:
float KahanSum( const float A[], int n )
{
float sum=0; // C, Y & T are now unused
for (int i=0; i<n; i++)
sum = sum + A[i];
return sum;
}
儘管轉換後的演算法更快,但它根本沒有準確表達程式員的意圖。精心設計的誤差修正已經 被完全消除,只剩下一個具有所有其關聯誤差的簡單的直接求和演算法。
當然,完善的 C++ 編譯器知道實數演算法的代數規則通常並不適用於浮點演算法。然而,即使是完善 的 C++ 編譯器,也可能錯誤地解釋程式員的意圖。
考慮一種常見的最佳化措施,它試圖在寄存器中存放儘可能多的值(稱為“登記”值)。在 KahanSum 樣本中,這一最佳化可能試圖登記變數 C、Y 和 T,因為這些變數僅在迴圈體內使用。如果寄存器精度為 52 位(雙精確度)而不是 23 位(單精確度),這一最佳化可以有效地將 C、Y 和 T 的類 型提升為 double。如果沒有以同樣的方式登記 sum 變數,則它仍將編 碼為單精確度。這會將 KahanSum 的語義轉換為下面的語義
float KahanSum( const float A[], int n )
{
float sum=0;
double C=0, Y, T; // now held in-register
for (int i=0; i<n; i++)
{
Y = A[i] - C;
T = sum + Y;
C = T - sum - Y;
sum = (float) T;
}
return sum;
}
儘管現在 Y、T 和 C 以更高的精度進行計算,但新的編碼可能產生精確性較低的結果,具體取決 於 A[] 中的值。因而,即使看起來無害的最佳化也可能具有消極的後果。
這些種類的最佳化問題並不局限於“棘手”的浮點代碼。即使是簡單的浮點演算法,在經過錯誤的最佳化 後也可能失敗。考慮一個簡單的直接求和演算法:
float Sum( const float A[], int n )
{
float sum=0;
for (int i=0; i<n; i++)
sum = sum + A[i];
return sum;
}