【问题标题】:Optimize for fast multiplication but slow addition: FMA and doubledouble优化快速乘法但慢加法:FMA 和 doubledouble
【发布时间】:2015-08-14 21:49:27
【问题描述】:

当我第一次获得 Haswell 处理器时,我尝试实施 FMA 来确定 Mandelbrot 集。主要算法是这样的:

intn = 0;
for(int32_t i=0; i<maxiter; i++) {
    floatn x2 = square(x), y2 = square(y); //square(x) = x*x
    floatn r2 = x2 + y2;
    booln mask = r2<cut; //booln is in the float domain non integer domain
    if(!horizontal_or(mask)) break; //_mm256_testz_pd(mask)
    n -= mask
    floatn t = x*y; mul2(t); //mul2(t): t*=2
    x = x2 - y2 + cx;
    y = t + cy;
}

这将确定 n 像素是否在 Mandelbrot 集中。所以对于双浮点它运行超过 4 个像素(floatn = __m256d,intn = __m256i)。这需要 4 次 SIMD 浮点乘法和 4 次 SIMD 浮点加法。

然后我修改它以像这样与 FMA 一起使用

intn n = 0; 
for(int32_t i=0; i<maxiter; i++) {
    floatn r2 = mul_add(x,x,y*y);
    booln mask = r2<cut;
    if(!horizontal_or(mask)) break;
    add_mask(n,mask);
    floatn t = x*y;
    x = mul_sub(x,x, mul_sub(y,y,cx));
    y = mul_add(2.0f,t,cy);
}

其中 mul_add 调用 _mm256_fmad_pd 和 mul_sub 调用 _mm256_fmsub_pd。此方法使用 4 个 FMA SIMD 操作和两个 SIMD 乘法,这比没有 FMA 的算术操作少了两次。此外,FMA 和乘法可以使用两个端口,而加法只能使用一个。

为了减少我的测试偏差,我放大了一个完全在 Mandelbrot 集中的区域,因此所有值都是 maxiter。在这种情况下使用 FMA 的方法大约快 27%。 这当然是一种改进,但是从 SSE 到 AVX 使我的性能翻了一番,所以我希望使用 FMA 可能再提高两倍。

但后来我找到了this 关于 FMA 的答案,上面写着

fused-multiply-add 指令的重要方面是(实际上)中间结果的无限精度。这有助于提高性能,但不是因为两个操作被编码在一条指令中 - 它有助于提高性能,因为中间结果的几乎无限精度有时很重要,并且在这种级别的普通乘法和加法中恢复非常昂贵精度确实是程序员所追求的。

后面给出了double*double到double-double乘法的例子

high = a * b; /* double-precision approximation of the real product */
low = fma(a, b, -high); /* remainder of the real product */

据此,我得出结论,我实施 FMA 不是最优的,因此我决定实施 SIMD 双双。我根据论文Extended-Precision Floating-Point Numbers for GPU Computation实现了double-double。该纸是用于双浮动的,所以我将其修改为双双。此外,我没有将一个双精度值封装到 SIMD 寄存器中,而是将 4 个双精度值封装到一个 AVX 高寄存器和一个 AVX 低寄存器中。

对于 Mandelbrot 集,我真正需要的是双双乘法和加法。在那篇论文中,这些是df64_add 和df64_mult 函数。 下图显示了software FMA(左)和硬件 FMA(右)的 df64_mult 函数的程序集。这清楚地表明,硬件 FMA 对双倍乘法是一个很大的改进。

那么硬件 FMA 在双双 Mandelbrot 集计算中的表现如何呢? 答案是,这仅比使用软件 FMA 快 15%。这比我希望的要少得多。 双双 Mandelbrot 计算需要 4 次双双加法和四次双双乘法(x*x、y*y、x*y 和 2*(x*y))。但是,2*(x*y) multiplication is trivial for double-double 所以这个乘法可以忽略成本。因此,我认为使用硬件 FMA 的改进如此之小的原因是计算以缓慢的双双加法为主(见下面的汇编)。

过去,乘法比加法慢(程序员使用了几种技巧来避免乘法),但在 Haswell 中,情况似乎正好相反。不仅因为 FMA,还因为乘法可以使用两个端口,而加法只能使用一个。

所以我的问题(最后)是:

  1. 当加法比乘法慢时如何优化?
  2. 有没有一种代数方法可以改变我的算法以使用更多的乘法 和更少的添加?我知道有方法可以做相反的事情,例如(x+y)*(x+y) - (x*x+y*y) = 2*x*y 使用两次加法来减少一次乘法。
  3. 有没有办法简化 df64_add 函数(例如使用 FMA)?

如果有人想知道 double-double 方法比 double 慢十倍左右。我认为这还不错,就好像有一个硬件四精度类型一样,它的速度可能至少是双精度类型的两倍,所以我的软件方法比我对硬件的预期慢五倍(如果它存在的话)。

df64_add大会

vmovapd 8(%rsp), %ymm0
movq    %rdi, %rax
vmovapd 72(%rsp), %ymm1
vmovapd 40(%rsp), %ymm3
vaddpd  %ymm1, %ymm0, %ymm4
vmovapd 104(%rsp), %ymm5
vsubpd  %ymm0, %ymm4, %ymm2
vsubpd  %ymm2, %ymm1, %ymm1
vsubpd  %ymm2, %ymm4, %ymm2
vsubpd  %ymm2, %ymm0, %ymm0
vaddpd  %ymm1, %ymm0, %ymm2
vaddpd  %ymm5, %ymm3, %ymm1
vsubpd  %ymm3, %ymm1, %ymm6
vsubpd  %ymm6, %ymm5, %ymm5
vsubpd  %ymm6, %ymm1, %ymm6
vaddpd  %ymm1, %ymm2, %ymm1
vsubpd  %ymm6, %ymm3, %ymm3
vaddpd  %ymm1, %ymm4, %ymm2
vaddpd  %ymm5, %ymm3, %ymm3
vsubpd  %ymm4, %ymm2, %ymm4
vsubpd  %ymm4, %ymm1, %ymm1
vaddpd  %ymm3, %ymm1, %ymm0
vaddpd  %ymm0, %ymm2, %ymm1
vsubpd  %ymm2, %ymm1, %ymm2
vmovapd %ymm1, (%rdi)
vsubpd  %ymm2, %ymm0, %ymm0
vmovapd %ymm0, 32(%rdi)
vzeroupper
ret

【问题讨论】:

  • 正如 Agner Fog 在他的一些优化资源中指出的(agner.org/optimize,但我不记得确切的文件或页面),有时 FMA 会使事情变慢。我想我记得那个例子是x*x + y*y,对于某些英特尔处理器型号,如果实现为 mul-fma,则延迟会使时序比包含更多并行性的 mul-mul-add 更差。
  • @PascalCuoq,到目前为止,我一直在使用IACA,然后查看时间。对于双倍来说,它主要是一个猜谜游戏,因为对于 Mandelbrot 集,FMA 有很多排列。使用 double-double 很清楚在哪里使用它。但主要问题是双双的加法。与乘法相比,它是如此缓慢。可以做什么?
  • 一个正确编程的双双加法包含 20 个基本的浮点运算。您可能会遇到使用较少指令的版本,但是当您需要和想要它时,这些指令会精确地失去准确性,这是对数量几乎相同的操作数的有效减法。双倍加法的加速需要硬件改进。最大幅度和最小幅度运算有一点帮助,单次舍入的三输入加法有很大帮助。一些处理器架构提供前者,我不知道有哪个处理器架构提供后者,
  • 如果您的准确度比 double 高一点,您可以通过对 x2hi 和 y2hi(6 次失败)进行 Knuth 2-sum 来计算 x2hi + x2lo + y2hi + y2lo + cx,另一个Knuth 2-sum 与 cx(另外 6 次失败),然后将四个余数相加。您可以使用基于 FMA 的 double*double -> double double 乘积计算 2*x*y+cy,使用 cy 进行 Knuth 2-sum,然后将余项相加。 (我没有对此进行测试或认真分析,所以这个建议可能是垃圾。)
  • @Zboson:按幅度的最小值和最大值允许您使用“Fast2Sum”;如果x 的幅度大于y,你可以做hi = x+y; lo = (x-hi)+y; lo 的计算中的差和和是精确的。 “浮点算术手册”包含所有这些技巧以及更多内容。我发现 Ogita、Rump 和 Oishi 的“精确和和点积”很有教育意义,但它绝不是 Fast2Sum 的原始参考。

标签: assembly x86 floating-point fma double-double-arithmetic


【解决方案1】:

为了回答我的第三个问题,我找到了一个更快的双倍加法解决方案。我在论文Implementation of float-float operators on graphics hardware 中找到了另一种定义。

Theorem 5 (Add22 theorem) Let be ah+al and bh+bl the float-float arguments of the following
algorithm:
Add22 (ah ,al ,bh ,bl)
1 r = ah ⊕ bh
2 if | ah | ≥ | bh | then
3     s = ((( ah ⊖ r ) ⊕ bh ) ⊕ b l ) ⊕ a l
4 e l s e
5     s = ((( bh ⊖ r ) ⊕ ah ) ⊕ a l ) ⊕ b l
6 ( rh , r l ) = add12 ( r , s )
7 return (rh , r l)

这是我的实现方式(伪代码):

static inline doubledoublen add22(doubledoublen const &a, doubledouble const &b) {
    doublen aa,ab,ah,bh,al,bl;
    booln mask;
    aa = abs(a.hi);                //_mm256_and_pd
    ab = abs(b.hi); 
    mask = aa >= ab;               //_mm256_cmple_pd
    // z = select(cut,x,y) is a SIMD version of z = cut ? x : y;
    ah = select(mask,a.hi,b.hi);   //_mm256_blendv_pd
    bh = select(mask,b.hi,a.hi);
    al = select(mask,a.lo,b.lo);
    bl = select(mask,b.lo,a.lo);

    doublen r, s;
    r = ah + bh;
    s = (((ah - r) + bh) + bl ) + al;
    return two_sum(r,s);
}

Add22 的这个定义使用 11 个加法而不是 20 个,但它需要一些额外的代码来确定是否为 |ah| &gt;= |bh|。 Here is a discussion on how to implement SIMD minmag and maxmag functions。幸运的是,大部分附加代码没有使用端口 1。现在只有 12 条指令进入端口 1,而不是 20。

这是新 Add22 的吞吐量分析表IACA

Throughput Analysis Report
--------------------------
Block Throughput: 12.05 Cycles       Throughput Bottleneck: Port1

Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
|  Port  |  0   -  DV  |  1   |  2   -  D   |  3   -  D   |  4   |  5   |  6   |  7   |
---------------------------------------------------------------------------------------
| Cycles | 0.0    0.0  | 12.0 | 2.5    2.5  | 2.5    2.5  | 2.0  | 10.0 | 0.0  | 2.0  |
---------------------------------------------------------------------------------------


| Num Of |                    Ports pressure in cycles                     |    |
|  Uops  |  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |  6  |  7  |    |
---------------------------------------------------------------------------------
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | vmovapd ymm3, ymmword ptr [rip]
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | vmovapd ymm0, ymmword ptr [rdx]
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | vmovapd ymm4, ymmword ptr [rsi]
|   1    |           |     |           |           |     | 1.0 |     |     |    | vandpd ymm2, ymm4, ymm3
|   1    |           |     |           |           |     | 1.0 |     |     |    | vandpd ymm3, ymm0, ymm3
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vcmppd ymm2, ymm3, ymm2, 0x2
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | vmovapd ymm3, ymmword ptr [rsi+0x20]
|   2    |           |     |           |           |     | 2.0 |     |     |    | vblendvpd ymm1, ymm0, ymm4, ymm2
|   2    |           |     |           |           |     | 2.0 |     |     |    | vblendvpd ymm4, ymm4, ymm0, ymm2
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | vmovapd ymm0, ymmword ptr [rdx+0x20]
|   2    |           |     |           |           |     | 2.0 |     |     |    | vblendvpd ymm5, ymm0, ymm3, ymm2
|   2    |           |     |           |           |     | 2.0 |     |     |    | vblendvpd ymm0, ymm3, ymm0, ymm2
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm3, ymm1, ymm4
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm2, ymm1, ymm3
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm1, ymm2, ymm4
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm1, ymm1, ymm0
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm0, ymm1, ymm5
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm2, ymm3, ymm0
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm1, ymm2, ymm3
|   2^   |           |     |           |           | 1.0 |     |     | 1.0 |    | vmovapd ymmword ptr [rdi], ymm2
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm0, ymm0, ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm1, ymm2, ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm3, ymm3, ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm0, ymm3, ymm0
|   2^   |           |     |           |           | 1.0 |     |     | 1.0 |    | vmovapd ymmword ptr [rdi+0x20], ymm0

这是旧的吞吐量分析

Throughput Analysis Report
--------------------------
Block Throughput: 20.00 Cycles       Throughput Bottleneck: Port1

Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
|  Port  |  0   -  DV  |  1   |  2   -  D   |  3   -  D   |  4   |  5   |  6   |  7   |
---------------------------------------------------------------------------------------
| Cycles | 0.0    0.0  | 20.0 | 2.0    2.0  | 2.0    2.0  | 2.0  | 0.0  | 0.0  | 2.0  |
---------------------------------------------------------------------------------------

| Num Of |                    Ports pressure in cycles                     |    |
|  Uops  |  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |  6  |  7  |    |
---------------------------------------------------------------------------------
|   1    |           |     | 1.0   1.0 |           |     |     |     |     |    | vmovapd ymm0, ymmword ptr [rsi]
|   1    |           |     |           | 1.0   1.0 |     |     |     |     |    | vmovapd ymm1, ymmword ptr [rdx]
|   1    |           |     | 1.0   1.0 |           |     |     |     |     |    | vmovapd ymm3, ymmword ptr [rsi+0x20]
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm4, ymm0, ymm1
|   1    |           |     |           | 1.0   1.0 |     |     |     |     |    | vmovapd ymm5, ymmword ptr [rdx+0x20]
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm2, ymm4, ymm0
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm1, ymm1, ymm2
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm2, ymm4, ymm2
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm0, ymm0, ymm2
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm2, ymm0, ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm1, ymm3, ymm5
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm6, ymm1, ymm3
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm5, ymm5, ymm6
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm6, ymm1, ymm6
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm1, ymm2, ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm3, ymm3, ymm6
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm2, ymm4, ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm3, ymm3, ymm5
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm4, ymm2, ymm4
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm1, ymm1, ymm4
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm0, ymm1, ymm3
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vaddpd ymm1, ymm2, ymm0
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm2, ymm1, ymm2
|   2^   |           |     |           |           | 1.0 |     |     | 1.0 |    | vmovapd ymmword ptr [rdi], ymm1
|   1    |           | 1.0 |           |           |     |     |     |     | CP | vsubpd ymm0, ymm0, ymm2
|   2^   |           |     |           |           | 1.0 |     |     | 1.0 |    | vmovapd ymmword ptr [rdi+0x20], ymm0

如果除了 FMA 之外还有三个操作数单舍入模式指令,则更好的解决方案是。在我看来,

应该有单一的舍入模式说明
a + b + c
a * b + c //FMA - this is the only one in x86 so far
a * b * c

【讨论】:

  • a*b*c 带给你什么?三之和使双倍加法变得微不足道,FMA 使双倍双倍乘法变得微不足道,但a*b*c 似乎没有这样的明显目的。
  • @tmyklebu,我不知道。可能你是对的。对于双双,我同意你的观点,a*b*c 似乎没有意义。我想知道这三个操作数一舍入模式指令还有哪些其他应用程序有用?我只用过双倍。
  • @tmyklebu,实际上想想,唯一需要的是a + b - c 和a*b - c。
【解决方案2】:

为了加快算法速度,我使用了基于 2 fma、1 mul 和 2 add 的简化版本。我以这种方式处理 8 次迭代。然后计算逃逸半径并在必要时回滚最后 8 次迭代。

以下用 x86 内部函数编写的关键循环 X = X^2 + C 被编译器很好地展开,展开后您会发现这 2 个 FMA 操作相互之间没有严重的依赖关系。

//  IACA_START;
for (j = 0; j < 8; j++) {
    Xrm = _mm256_mul_ps(Xre, Xim);
    Xtt = _mm256_fmsub_ps(Xim, Xim, Cre);
    Xrm = _mm256_add_ps(Xrm, Xrm);
    Xim = _mm256_add_ps(Cim, Xrm);
    Xre = _mm256_fmsub_ps(Xre, Xre, Xtt);
}       // for
//  IACA_END;

然后我计算逃逸半径(|X|

cmp = _mm256_mul_ps(Xre, Xre);
cmp = _mm256_fmadd_ps(Xim, Xim, cmp);
cmp = _mm256_cmp_ps(cmp, vec_threshold, _CMP_LE_OS);
if (_mm256_testc_si256((__m256i) cmp, vec_one)) {
    i += 8;
    continue;
}

您提到“加法很慢”,这并不完全正确,但您是对的,在最近的架构上,乘法吞吐量随着时间的推移变得越来越高。

乘法延迟和依赖性是关键。 FMA 具有 1 个周期的吞吐量和 5 个周期的延迟。独立的 FMA 指令的执行可以重叠。

基于乘法结果的加法得到完整的延迟命中。

因此,您必须通过“代码拼接”来打破这些直接依赖关系,并在同一个循环中计算 2 个点,然后在与 IACA 核对会发生什么之前交错代码。下面的代码有2组变量(X0=X0^2+C0,X1=X1^2+C1后缀为0和1),开始填充FMA孔

for (j = 0; j < 8; j++) {
    Xrm0 = _mm256_mul_ps(Xre0, Xim0);
    Xrm1 = _mm256_mul_ps(Xre1, Xim1);
    Xtt0 = _mm256_fmsub_ps(Xim0, Xim0, Cre);
    Xtt1 = _mm256_fmsub_ps(Xim1, Xim1, Cre);
    Xrm0 = _mm256_add_ps(Xrm0, Xrm0);
    Xrm1 = _mm256_add_ps(Xrm1, Xrm1);
    Xim0 = _mm256_add_ps(Cim0, Xrm0);
    Xim1 = _mm256_add_ps(Cim1, Xrm1);
    Xre0 = _mm256_fmsub_ps(Xre0, Xre0, Xtt0);
    Xre1 = _mm256_fmsub_ps(Xre1, Xre1, Xtt1);
}       // for

总结一下,

  • 您可以在关键循环中将指令数量减半
  • 您可以添加更多独立指令,并利用乘法和融合乘加的高吞吐量和低延迟。

【讨论】:

  • 你所说的“代码拼接”看起来像like software pipelining。但是,是的,保留更多数据以隐藏 FP 延迟会暴露更多指令级并行性。展开更多点是将原始问题中的数据并行性转化为 SIMD + ILP 的好方法。
  • 您的 FMA 延迟/吞吐量数字似乎来自 Ryzen 或 Bulldozer 系列。但是你是什么意思“基于乘法结果的加法得到完整的延迟命中”?任何相关指令都会受到全部延迟的影响; FMA->FMA 与 MUL->ADD 具有相同的延迟。 (即使在推土机上,FMA 单元内也有特殊的快速转发,使其变为 5c 而不是 6c。agner.org/optimize)。无论如何,所有具有 FMA 的 Intel CPU 的 FMA 吞吐量为 0.5c。但是在 Skylake 之前的 CPU 上,ADD 的吞吐量为 1c(它在 FMA 单元上完成所有操作,将其降低到 4c lat)。
  • 0.5c 基于具有 2 个 FMA 引擎的单元。很难为 haswell、skylake、coffeelake、cubilake 的每个变体“填充管道”......我使用简单的阈值 "ideal number of fma" = "latency" / "throughput" 。我知道我建议的代码不是最优的,因为超线程仍然可以填补漏洞。如果编译器可以避免将实时寄存器溢出到内存中(使用 skylake 的 avx512vl 变体 ...),流水线计算 3 x 8 点会更好。
  • 您使用的是 256 位 FMA。 所有支持 FMA 的 Intel CPU 有两个 256 位 FMA 单元。您正在考虑 Skylake-AVX512,其中一些只有一个 512 位 FMA 单元。 (SKX 在 512 位微指令运行时关闭端口 1 上的向量 ALU。端口 0/端口 1 上的两个 256 位 FMA 单元作为一个 512 位 FMA 单元工作。某些型号有一个额外的 512 位端口 5 上的 FMA 单元仅对 512 位微指令激活。)
  • @Pierre 我的问题是关于使用double-double 来提高精度。我已经知道如何优化double。放大过去 10^-7 后,您的方法将失去精度。但是我发现了一种使用扰动理论的 Mandelbrot 集更好的方法,它将限制因素从尾数移到指数。所以现在我可以转到10^-308,这比double-double 好得多,double-double 不会改变指数的精度。无论如何,据我所知,你的回答根本没有回答我的问题。
【解决方案3】:

您提到以下代码:

vsubpd  %ymm0, %ymm4, %ymm2
vsubpd  %ymm2, %ymm1, %ymm1  <-- immediate dependency ymm2
vsubpd  %ymm2, %ymm4, %ymm2
vsubpd  %ymm2, %ymm0, %ymm0  <-- immediate dependency ymm2
vaddpd  %ymm1, %ymm0, %ymm2  <-- immediate dependency ymm0
vaddpd  %ymm5, %ymm3, %ymm1
vsubpd  %ymm3, %ymm1, %ymm6  <-- immediate dependency ymm1
vsubpd  %ymm6, %ymm5, %ymm5  <-- immediate dependency ymm6
vsubpd  %ymm6, %ymm1, %ymm6  <-- dependency ymm1, ymm6
vaddpd  %ymm1, %ymm2, %ymm1
vsubpd  %ymm6, %ymm3, %ymm3  <-- dependency ymm6
vaddpd  %ymm1, %ymm4, %ymm2 
vaddpd  %ymm5, %ymm3, %ymm3  <-- dependency ymm3
vsubpd  %ymm4, %ymm2, %ymm4 
vsubpd  %ymm4, %ymm1, %ymm1  <-- immediate dependency ymm4
vaddpd  %ymm3, %ymm1, %ymm0  <-- immediate dependency ymm1, ymm3
vaddpd  %ymm0, %ymm2, %ymm1  <-- immediate dependency ymm0
vsubpd  %ymm2, %ymm1, %ymm2  <-- immediate dependency ymm1

如果仔细检查,这些大多是依赖操作,不符合延迟/吞吐量效率的基本规则。大多数指令取决于前一条或两条指令的结果。该序列包含 30 个周期的关键路径(大约 9 或 10 条关于“3 个周期延迟”/“1 个周期吞吐量”的指令)。

您的 IACA 在关键路径中报告“CP”=> 指令,评估成本为 20 个周期的吞吐量。您应该获得延迟报告,因为如果您对执行速度感兴趣,它是最重要的。

要消除这条关键路径的成本,如果编译器无法执行此操作,您必须交错大约 20 条类似的指令(例如,因为您的 double-double 代码位于没有 -flto 优化和 vzeroupper 函数处处处编译的单独库中进入和退出,矢量化器只适用于内联代码)。

一种可能性是并行运行 2 次计算(请参阅上一篇文章中的代码拼接以改进流水线)

如果我假设您的双双代码看起来像这种“标准”实现

// (r,e) = x + y
#define two_sum(x, y, r, e) 
    do { double t; r = x + y; t = r - x; e = (x - (r - t)) + (y - t); } while (0)
#define two_difference(x, y, r, e) \
    do { double t; r = x - y; t = r - x; e = (x - (r - t)) - (y + t); } while (0)
.....

然后您必须考虑以下代码,其中指令以非常精细的方式交错。

// (r1, e1) = x1 + y1, (r2, e2) x2 + y2
#define two_sum(x1, y1, x2, y2, r1, e1, r2, e2) 
    do { double t1, t2 \
    r1 = x1 + y1; r2 = x2 + y2; \
    t1 = r1 - x1; t2 = r2 - x2; \
    e1 = (x1 - (r1 - t1)) + (y1 - t1); e2 = (x2 - (r2 - t2)) + (y2 - t2);  \
} while (0)
....

然后这将创建类似于以下代码的代码(延迟报告中的关键路径大致相同,大约 35 条指令)。有关运行时的详细信息,乱序执行应该越过它而不会停止。

vsubsd  %xmm2, %xmm0, %xmm8
vsubsd  %xmm3, %xmm1, %xmm1
vaddsd  %xmm4, %xmm4, %xmm4
vaddsd  %xmm5, %xmm5, %xmm5
vsubsd  %xmm0, %xmm8, %xmm9
vsubsd  %xmm9, %xmm8, %xmm10
vaddsd  %xmm2, %xmm9, %xmm2
vsubsd  %xmm10, %xmm0, %xmm0
vsubsd  %xmm2, %xmm0, %xmm11
vaddsd  %xmm14, %xmm4, %xmm2
vaddsd  %xmm11, %xmm1, %xmm12
vsubsd  %xmm4, %xmm2, %xmm0
vaddsd  %xmm12, %xmm8, %xmm13
vsubsd  %xmm0, %xmm2, %xmm11
vsubsd  %xmm0, %xmm14, %xmm1
vaddsd  %xmm6, %xmm13, %xmm3
vsubsd  %xmm8, %xmm13, %xmm8
vsubsd  %xmm11, %xmm4, %xmm4
vsubsd  %xmm13, %xmm3, %xmm15
vsubsd  %xmm8, %xmm12, %xmm12
vaddsd  %xmm1, %xmm4, %xmm14
vsubsd  %xmm15, %xmm3, %xmm9
vsubsd  %xmm15, %xmm6, %xmm6
vaddsd  %xmm7, %xmm12, %xmm7
vsubsd  %xmm9, %xmm13, %xmm10
vaddsd  16(%rsp), %xmm5, %xmm9
vaddsd  %xmm6, %xmm10, %xmm15
vaddsd  %xmm14, %xmm9, %xmm10
vaddsd  %xmm15, %xmm7, %xmm13
vaddsd  %xmm10, %xmm2, %xmm15
vaddsd  %xmm13, %xmm3, %xmm6
vsubsd  %xmm2, %xmm15, %xmm2
vsubsd  %xmm3, %xmm6, %xmm3
vsubsd  %xmm2, %xmm10, %xmm11
vsubsd  %xmm3, %xmm13, %xmm0

总结:

  • 内联您的双双源代码:编译器和矢量化器由于 ABI 约束而无法跨函数调用进行优化,并且由于担心别名而无法跨内存访问。

    李>
  • 缝合代码以平衡吞吐量和延迟并最大化 CPU 端口使用率(并且还最大化每个周期的指令),只要编译器不会将太多寄存器溢出到内存。

您可以使用 perf 实用程序(软件包 linux-tools-generic 和 linux-cloud-tools-generic)跟踪优化影响,以获取执行的指令数和每个周期的指令数。

【讨论】:

  • 乱序执行将隐藏依赖链只要它们不是循环携带的,或者如果存在任何指令级并行性则部分隐藏它们。如果循环承载的 dep 链的延迟是瓶颈,而不是前端或执行端口,IACA 报告会说“依赖”或“延迟”(我忘记了)。现代 x86 上的指令调度通常不是很重要。
  • @PeterCordes 你是对的,但是在 Mandelbrot 示例的上下文中,下一个循环的执行取决于当前循环的结果。我建议的解决方案是开始在这样简单的循环中交错 2 个独立的依赖链(并且每个依赖链都已经矢量化)。在 2 个超线程上运行相同的代码是填补延迟漏洞的另一种方法。
猜你喜欢
  • 2014-07-10
  • 1970-01-01
  • 2011-03-21
  • 2019-08-10
  • 1970-01-01
  • 1970-01-01
  • 2022-06-26
  • 1970-01-01
  • 2017-01-25
相关资源
最近更新 更多