迭代可汗求和的优化实现

Optimal implementation of iterative Kahan summation

本文关键字:优化 实现 和的 求和 迭代      更新时间:2023-10-16

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

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

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

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

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

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

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
extern "C" void csumasm(double&, double, double&);
__asm__(
"csumasm:n"
"movsd  (%rcx), %xmm0n" //xmm0 = a
"subsd  (%r8), %xmm1n"  //xmm1 - r8 (c) | y = b-c
"movapd %xmm0, %xmm2n"  
"addsd  %xmm1, %xmm2n"  //xmm2 + xmm1 (y) | b = a+y
"movapd %xmm2, %xmm3n" 
"subsd  %xmm0, %xmm3n"  //xmm3 - xmm0 (a) | b - a
"movapd %xmm3, %xmm0n"  
"subsd  %xmm1, %xmm0n"  //xmm0 - xmm1 (y) | - y
"movsd  %xmm0, (%r8)n"  //xmm0 to c
"movsd  %xmm2, (%rcx)n" //b to a
"retn"
);
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)nn",sum1,sum1-sum3);
printf("Kahan compensated summation [g++]:n %.16e (error: %.16e)nn",sum2,sum2-sum3);
printf("Kahan compensated summation [asm]:n %.16en",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"函数可以。
  • 总和必须作为迭代过程计算(校正后的项必须应用于每个添加项)。这是因为预期的求和函数采用的输入取决于前一个总和。
  • 预期的求和函数被无限期调用,每秒调用数亿次,这促使人们追求高性能的低级实现。
  • 由于性能原因,更高精度的算法(如长双精度、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++ 指定了类似于 GCC-fno-fast-math的 FP 数学规则。 仅用-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具有与addsdsubsd相同的性能(1 uop,3 到 5 个周期延迟,具体取决于微架构:https://agner.org/optimize/)。

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

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


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

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

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

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

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

但实际上到目前为止,您的赌注是让 GCC 在没有-ffast-math的情况下编译文件或函数。 https://gcc.gnu.org/wiki/DontUseInlineAsm。 这让编译器在 AVX 可用时避免了movaps指令,此外还可以让它在展开时更好地优化。

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

但是,通过将包含求和循环的整个函数放在单独的文件中来禁用快速数学会好得多。 您可能会在选择非内联函数调用边界需要的位置时遇到困难,基于编译一些具有快速数学代码而其他代码则不使用快速数学的代码。

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


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

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


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