迭代Kahan求和的最佳实现

时间:2019-08-01 03:51:07

标签: c++ x86-64 sse inline-assembly fast-math

简介
Kahan求和/补偿求和是一种解决编译器无法尊重数字关联属性的技术。截断误差导致(a + b)+ c不完全等于a +(b + c),从而在较长的和序列上积累了不希望的相对误差,这是科学计算中的常见障碍。

任务
我希望最佳实现Kahan求和。我怀疑手工制作的汇编代码可能会达到最佳性能。

尝试
下面的代码使用三种方法计算范围为[0,1]的1000个随机数之和。

  1. 标准求和:天真的实现,它累积的均方根相对误差随着O(sqrt(N))的增长而增加

  2. Kahan求和[g ++] :使用c / c ++函数“ csum”的补偿求和。注释中的解释。请注意,某些编译器可能具有使该实现无效的默认标志(请参见下面的输出)。

  3. Kahan求和[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

1 个答案:

答案 0 :(得分:4)

您可以将不需要使用 的函数放在单独的文件中,而该文件无需-ffast-math就可以使用。{}就像csum循环一样。

可能还可以使用-ffast-math,但是https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html表示不幸的是,优化级的杂注和属性“不适合生产代码”。

更新:问题的一部分显然是基于对__attribute__((optimize("no-fast-math")))的不安全或某种误解的误解。它是; ISO C ++指定了FP数学规则,例如GCC的-O3。仅用-fno-fast-math编译所有内容显然可以使OP的代码快速安全地运行。有关诸如OpenMP之类的变通方法,请参见此答案的底部,以在不实际启用-O3的情况下获得代码某些部分的快速计算的好处。

ICC默认为快速路径,因此您必须专门启用FP = strict以使其在-O3下安全运行,但是gcc / clang默认为完全严格的FP,而不考虑其他优化设置。 (-ffast-math = -Ofast除外)


通过保持总计的一个(或四个)向量和相等数量的补偿向量,您应该能够向量化Kahan总和。您可以使用内部函数来做到这一点(只要您不为该文件启用快速计数)。

例如每条指令使用SSE2 -O3 -ffast-math进行2个打包加法。或AVX __m128d。在现代x86上,__m256d / addpd具有与subpdaddsd相同的性能(1 uop,3至5个周期的延迟,具体取决于微体系结构:https://agner.org/optimize/

因此,您实际上是在并行执行8个补偿求和,每个求和得到第8个输入元素。

subsd即时生成随机数要比从内存中读取随机数慢得多。如果您的正常用例在内存中有数据,则应该对其进行基准测试。否则我想标量很有趣。


如果要使用内联汇编,最好以内联方式实际使用它,这样您就可以在带有扩展asm的XMM寄存器中获得多个输入和多个输出,而不是通过内存进行存储/重新加载。

定义一个通过引用实际使用args的独立函数,看起来会降低性能。 (特别是当它甚至没有返回任何一个作为返回值时,避免使用存储/重载链之一)。即使只是进行函数调用,也会破坏许多寄存器,从而造成大量开销。 (在Windows x64中不如在x86-64 System V中那样糟糕,其中所有 XMM regs都被调用,以及更多的整数regs。)

您的独立函数还特定于Windows x64调用约定,因此它比函数内部的嵌入式asm的移植性差。

顺便说一句, fun()设法仅用两个clang指令实现了csum(double&, double, double&): ,而不是您的asm中的3条指令(我假设您从GCC的asm输出)。 https://godbolt.org/z/lw6tug。如果您可以假设AVX可用,则可以避免使用。

顺便说一句,movapd小1字节,应改为使用。对于movapsdouble,没有CPU具有单独的数据域/转发网络,只有vec-FP与vec-int(相对于GP整数)

但是到目前为止,您真正的选择是让GCC编译没有float的文件或函数。 https://gcc.gnu.org/wiki/DontUseInlineAsm 。这样一来,除了使AVX可以在展开时更好地进行优化之外,编译器还可以避免使用-ffast-math指令。

如果您愿意接受每个元素的函数调用的开销,则最好让编译器通过将movaps放在单独的文件中来生成该组件。 (希望链接时优化尊重一个文件的csum,也许是因为不内联该函数。)

但是最好通过将 it 放在单独的文件中来禁用包含求和循环的整个函数的快速数学运算。您可能会因为编译一些带有快速运算符的代码而没有编译一些带有快速运算符的代码而难以选择非内联函数调用的边界。

理想地使用-fno-fast-math编译所有代码,并进行配置文件引导的优化。此外,-O3 -march=native链接时优化可启用跨文件内联。


-flto打破了Kahan求和也就不足为奇了。将FP数学视为关联的是使用快速数学的主要原因之一。如果您需要-ffast-math的其他部分(例如-ffast-math-fno-math-errno,以便数学函数可以更好地内联,请手动启用它们。这些基本上都是安全的,是个好主意。没有人在调用-fno-trapping-math之后检查errno,因此为某些输入设置errno的要求只是对C的严重错误设计,不必要地给实现增加了负担。即使GCC的sqrt已损坏(默认情况下也处于打开状态)(它并不总是能够完全重现未屏蔽的FP异常的数量),所以it should really be off by default。关闭此功能不会启用任何会破坏NaN传播的优化,只会告诉GCC异常数量不是可见的副作用。

或者为您的Kahan求和文件尝试-ftrapping-math,但这是自动向量化涉及归约法的FP循环所需要的主要工具,并且在其他情况下会有所帮助。但是,您仍然可以获得其他一些有价值的优化。


获得通常需要快速运算的优化的另一种方法是-ffast-math -fno-associative-math ,即使在没有 自动编译的文件中,也可以使用OpenMP进行自动矢量化。您可以声明一个用于减少的累加器变量,以使gcc对其上的操作进行重新排序,就好像它们是关联的一样。