簡體   English   中英

迭代Kahan求和的最佳實現

[英]Optimal implementation of iterative Kahan summation

介紹
Kahan求和/補償求和是一種解決編譯器無法尊重數字關聯屬性的技術。 截斷誤差導致(a + b)+ c不完全等於a +(b + c),從而在較長的和序列上積累了不希望的相對誤差,這是科學計算中的常見障礙。

任務
我希望最佳實現Kahan求和。 我懷疑使用手工匯編代碼可能會獲得最佳性能。

嘗試
下面的代碼使用三種方法計算范圍為[0,1]的1000個隨機數的總和。

  1. 標准求和 :天真的實現,其累積的均方根相對誤差隨着O(sqrt(N))的增長而增加

  2. Kahan sumsum [g ++] :使用c / c ++函數“ csum”的補償求和。 注釋中的解釋。 請注意,某些編譯器可能具有使該實現無效的默認標志(請參見下面的輸出)。

  3. Kahan sumsum [asm] :使用與“ csum”相同的算法實現為“ csumasm”的補償求和。 評論中的隱秘解釋。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

extern "C" void csumasm(double&, double, double&);
__asm__(
    "csumasm:\n"
    "movsd  (%rcx), %xmm0\n" //xmm0 = a
    "subsd  (%r8), %xmm1\n"  //xmm1 - r8 (c) | y = b-c
    "movapd %xmm0, %xmm2\n"  
    "addsd  %xmm1, %xmm2\n"  //xmm2 + xmm1 (y) | b = a+y
    "movapd %xmm2, %xmm3\n" 
    "subsd  %xmm0, %xmm3\n"  //xmm3 - xmm0 (a) | b - a
    "movapd %xmm3, %xmm0\n"  
    "subsd  %xmm1, %xmm0\n"  //xmm0 - xmm1 (y) | - y
    "movsd  %xmm0, (%r8)\n"  //xmm0 to c
    "movsd  %xmm2, (%rcx)\n" //b to a
    "ret\n"
);

void csum(double &a,double b,double &c) { //this function adds a and b, and passes c as a compensation term
    double y = b-c; //y is the correction of b argument
    b = a+y; //add corrected b argument to a argument. The output of the current summation
    c = (b-a)-y; //find new error to be passed as a compensation term
    a = b;
}

double fun(double fMin, double fMax){
    double f = (double)rand()/RAND_MAX;
    return fMin + f*(fMax - fMin); //returns random value
}

int main(int argc, char** argv) {
    int N = 1000;

    srand(0); //use 0 seed for each method
    double sum1 = 0;
    for (int n = 0; n < N; ++n)
        sum1 += fun(0,1);

    srand(0);
    double sum2 = 0;
    double c = 0; //compensation term
    for (int n = 0; n < N; ++n)
        csum(sum2,fun(0,1),c);

    srand(0);
    double sum3 = 0;
    c = 0;
    for (int n = 0; n < N; ++n)
        csumasm(sum3,fun(0,1),c);

    printf("Standard summation:\n %.16e (error: %.16e)\n\n",sum1,sum1-sum3);
    printf("Kahan compensated summation [g++]:\n %.16e (error: %.16e)\n\n",sum2,sum2-sum3);
    printf("Kahan compensated summation [asm]:\n %.16e\n",sum3);
    return 0;
}

-O3的輸出為:

Standard summation:
 5.1991955320902093e+002 (error: -3.4106051316484809e-013)

Kahan compensated summation [g++]:
 5.1991955320902127e+002 (error: 0.0000000000000000e+000)

Kahan compensated summation [asm]:
 5.1991955320902127e+002

輸出-O3 -ffast-math

Standard summation:
 5.1991955320902093e+002 (error: -3.4106051316484809e-013)

Kahan compensated summation [g++]:
 5.1991955320902093e+002 (error: -3.4106051316484809e-013)

Kahan compensated summation [asm]:
 5.1991955320902127e+002

很明顯,-ffast-math破壞了Kahan求和算法,這很不幸,因為我的程序需要使用-ffast-math。

  1. 是否可以為Kahan的補償總和構造更好/更快的asm x64代碼? 也許有一種聰明的方法可以跳過某些movapd指令?

  2. 如果沒有更好的asm代碼,是否有一種c ++方式可以實現Kahan求和,而該方法可以與-ffast-math一起使用而不會降級為朴素的求和? 也許對於編譯器而言,c ++實現通常更靈活地進行優化。

想法或建議表示贊賞。

更多信息

  • 不能內聯“ fun”的內容,但可以內聯“ csum”功能。
  • 該總和必須作為迭代過程來計算(更正項必須應用於每個單項加法)。 這是因為預期求和函數采用的輸入取決於先前的和。
  • 預期的求和函數被無限次地每秒調用數億次,這激發了對高性能低級實現的追求。
  • 由於性能原因,不應將諸如long double,float128或任意精度庫之類的高精度算術視為高精度解決方案。

編輯:內聯csum(沒有完整的代碼沒有多大意義,但僅供參考)

        subsd   xmm0, QWORD PTR [rsp+32]
        movapd  xmm1, xmm3
        addsd   xmm3, xmm0
        movsd   QWORD PTR [rsp+16], xmm3
        subsd   xmm3, xmm1
        movapd  xmm1, xmm3
        subsd   xmm1, xmm0
        movsd   QWORD PTR [rsp+32], xmm1

你可以把那些需要使用功能-ffast-math在被編譯一個單獨的文件(如CSUM環) -ffast-math

可能還可以使用__attribute__((optimize("no-fast-math"))) ,但是https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html表示優化級別的編譯指示和不幸的是,屬性不是“適合生產代碼”。

更新:問題的一部分顯然是基於對-O3並不安全或某種誤解的誤解? 它是; ISO C ++指定了FP數學規則,例如GCC的-fno-fast-math 僅使用-O3進行編譯就可以使OP的代碼快速安全地運行。 有關諸如OpenMP之類的變通方法, -ffast-math答案的底部,以在不實際啟用-ffast-math情況下獲得代碼某些部分的快速運算的好處。

ICC默認為快速路徑,因此您必須專門啟用FP = strict才能使用-O3使其安全,但是gcc / clang默認為完全嚴格的FP,而不考慮其他優化設置。 -Ofast = -O3 -ffast-math除外)


通過保持總計的一個(或四個)向量和相等數量的補償向量,您應該能夠向量化Kahan總和。 您可以使用內部函數來做到這一點(只要您不為該文件啟用快速計算)。

例如,每條指令使用SSE2 __m128d進行2個打包添加。 或AVX __m256d 在現代x86上, addpd / subpd具有與addsdsubsd相同的性能(1 subsd ,3到5個周期的延遲,具體取決於微體系結構: https : subsd )。

因此,您實際上是在並行進行8個補償求和,每個求和得到第8個輸入元素。

使用fun()生成隨機數要比從內存中讀取隨機數慢得多。 如果您的正常用例在內存中有數據,則應該對其進行基准測試。 否則我想標量很有趣。


如果要使用內聯匯編,最好以內聯方式實際使用它,這樣您就可以在帶有擴展匯編的XMM寄存器中獲得多個輸入和多個輸出,而不是通過內存進行存儲/重新加載。

定義一個實際上通過引用來接受args的獨立函數看起來會嚴重影響性能。 (特別是當它甚至沒有返回任何一個作為返回值時,避免使用存儲/重載鏈之一)。 即使只是進行函數調用,也會破壞許多寄存器,從而造成大量開銷。 (在Windows x64中不如在x86-64 System V中糟糕, 所有 XMM reg都被調用,以及更多的整數reg。)

另外,您的獨立函數特定於Windows x64調用約定,因此它比函數內部的內聯匯編的移植性差。

而且順便說一句, clang設法實現了csum(double&, double, double&):僅使用兩個movapd指令 ,而不是您的asm中的3 條指令 (我假設您是從GCC的asm輸出中復制的)。 https://godbolt.org/z/lw6tug 如果可以假定AVX可用,則可以避免任何情況。

順便說一句, movaps要小1個字節,而應改用。 沒有CPU具有用於doublefloat單獨數據域/轉發網絡,只有vec-FP與vec-int(相對於GP整數)

但是到目前為止,您真正的選擇是讓GCC編譯不帶-ffast-math的文件或函數。 https://gcc.gnu.org/wiki/DontUseInlineAsm 這使編譯器可以在AVX可用時避免使用movaps指令,此外還可以使其在展開時進行更好的優化。

如果您願意接受每個元素的函數調用的開銷,則最好讓編譯器通過將csum放在單獨的文件中來生成該asm。 (希望鏈接時優化對一個文件尊重-fno-fast-math ,也許是因為不內聯該函數。)

但是最好將包含求和循環的整個函數禁用快速運算,方法是將放入單獨的文件中。 您可能會因為編譯一些具有快速運算能力的代碼而沒有編譯一些具有快速運算能力的代碼,而難以選擇非內聯函數調用邊界的位置。

理想情況下,請使用-O3 -march=native以及配置文件引導的優化來編譯所有代碼。 還可以使用-flto鏈接時間優化來啟用跨文件內聯。


-ffast-math打破Kahan求和-ffast-math不足為奇了:將FP數學視為關聯是使用fast-math的主要原因之一。 如果您需要-ffast-math其他部分(例如-fno-math-errno-fno-trapping-math以便數學函數可以更好地內聯,請手動啟用它們。 這些基本上都是安全的,是個好主意。 沒有人在調用sqrt之后檢查errno ,因此為某些輸入設置errno的要求只是對C的嚴重錯誤設計,不必要地增加了實現。 即使GCC的-ftrapping-math損壞了,它也是默認情況下處於打開狀態(它並不總是准確地再現您未屏蔽任何FP時得到的FP異常的數量),因此默認情況下它應該確實處於關閉狀態 禁用它不會啟用任何會破壞NaN傳播的優化,只會告訴GCC異常數量不是可見的副作用。

或者為您的Kahan求和文件嘗試-ffast-math -fno-associative-math ,但這是自動向量化涉及歸約的FP循環所需要的主要工具,並且在其他情況下會有所幫助。 但是,您仍然可以獲得其他一些有價值的優化。


獲得通常需要快速運算的優化的另一種方法是#pragma omp simd ,即使在未經自動矢量化編譯的文件中也可以使用OpenMP 進行自動矢量化。 您可以聲明一個用於減少的累加器變量,以使gcc對其上的操作進行重新排序,就好像它們是關聯的一樣。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM