6

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

任务
我希望 Kahan 求和的最佳实施。我怀疑使用手工汇编代码可以实现最佳性能。

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

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

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

  3. Kahan summation [asm]:补偿求和实现为“csumasm”,使用与“csum”相同的算法。评论中的神秘解释。

#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
4

1 回答 1

4

您可以将不需要使用的函数-ffast-mathcsum 循环)放在一个单独的文件中,该文件无需-ffast-math.

可能您也可以使用__attribute__((optimize("no-fast-math"))),但不幸的是, https ://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html 说优化级别的编译指示和属性“不适合生产代码”。

更新:显然问题的一部分是基于-O3不安全的误解,还是什么?这是; ISO C++ 指定类似于 GCC 的 FP 数学规则-fno-fast-math。编译所有内容-O3显然可以使 OP 的代码快速安全地运行。有关 OpenMP 之类的解决方法,请参阅此答案的底部,以便在无需实际启用-ffast-math.

ICC 默认为快速路径,因此您必须专门启用 FP=strict 以使其与 -O3 一起安全,但 gcc/clang 默认为完全严格的 FP,无论其他优化设置如何。(除了-Ofast= -O3 -ffast-math


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

例如,每条指令使用 SSE2__m128d进行 2 次打包加法。或 AVX __m256d。在现代 x86 上,addpd/具有与和subpd相同的性能(1 uop,3 到 5 个周期延迟,具体取决于微架构:https ://agner.org/optimize/ )。addsdsubsd

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

动态生成随机数fun()比从内存中读取要慢得多。如果您的正常用例在内存中有数据,那么您应该对其进行基准测试。否则我猜标量很有趣。


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

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

此外,您的独立函数特定于 Windows x64 调用约定,因此它的可移植性不如函数内的内联 asm。

顺便说一句,clang设法csum(double&, double, double&):只用两条movapd指令来实现,而不是你的 asm 中的 3 条(我假设你是从 GCC 的 asm 输出中复制的)。 https://godbolt.org/z/lw6tug。如果您可以假设 AVX 可用,则可以避免任何情况。

顺便说一句,movaps要小 1 个字节,应该改用。double没有 CPU 对vs.有单独的数据域/转发网络float,只有 vec-FP 与 vec-int(与 GP 整数)

但到目前为止,您真正的赌注是让 GCC 编译没有-ffast-math. https://gcc.gnu.org/wiki/DontUseInlineAsm。这可以让编译器movaps在 AVX 可用时避免指令,此外还可以在展开时更好地优化。

如果您愿意为每个元素接受函数调用的开销,您不妨让编译器通过放入csum单独的文件来生成该 asm。(希望链接时优化尊重-fno-fast-math一个文件,也许不内联该函数。)

但是最好禁用包含求和循环的整个函数的快速数学,方法是把放在一个单独的文件中。你可能会被困在选择非内联函数调用边界的位置,基于编译一些带有快速数学的代码和其他没有的代码。

-O3 -march=native理想情况下,使用, 和配置文件引导的优化来编译所有代码。还有-flto链接时优化以启用跨文件内联。


-ffast-math打破 Kahan 求和并不奇怪:将 FP 数学视为关联是使用快速数学的主要原因之一。-ffast-math如果您需要类似的其他部分,-fno-math-errno以便-fno-trapping-math数学函数可以更好地内联,请手动启用它们。这些基本上总是安全的,而且是个好主意;errno没有人在调用后进行检查,sqrt因此为某些输入设置 errno 的要求只是 C 语言的严重错误设计,给实现带来了不必要的负担。默认情况下, GCC-ftrapping-math是打开的,即使它被破坏了(如果你取消屏蔽,它并不总是准确地重现你会得到的 FP 异常的数量)所以它应该默认关闭. 关闭它不会启用任何会破坏 NaN 传播的优化,它只会告诉 GCC 异常的数量不是可见的副作用。

或者也许尝试-ffast-math -fno-associative-math你的 Kahan 求和文件,但这是自动矢量化涉及减少的 FP 循环所需的主要文件,并在其他情况下有所帮助。但是,您仍然可以获得其他一些有价值的优化。


获得通常需要快速数学的优化的另一种方法是#pragma omp simd使用 OpenMP 启用自动矢量化,即使在没有自动矢量化的情况下编译的文件也是如此。你可以声明一个累加器变量来减少,让 gcc 对它的操作重新排序,就好像它们是关联的一样。

于 2019-08-01T04:26:32.170 回答