当我第一次使用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;
}
这确定Mandelbrot集中是否有n
像素。因此,对于双浮点,它运行超过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的回答
融合乘法 - 加法指令的重要方面是中间结果的(虚拟)无限精度。这有助于提高性能,但不是因为两个操作在一条指令中编码 - 它有助于提高性能,因为中间结果的几乎无限精度有时很重要,并且通过普通乘法和加法来恢复非常昂贵精确度正是程序员追求的目标。
后来给出了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实施了双重翻译。这篇论文用于双浮动,所以我修改它为双倍。此外,不是在SIMD寄存器中打包一个双倍值,而是将4个双倍值打包到一个AVX高位寄存器和一个AVX低位寄存器中。
对于Mandelbrot集合我真正需要的是双倍乘法和加法。在该论文中,这些是df64_add
和df64_mult
函数。
下图显示了software FMA(左)和硬件FMA(右)的df64_mult
函数的程序集。这清楚地表明硬件FMA是双倍乘法的重大改进。
那么硬件FMA在双倍Mandelbrot集合计算中的表现如何? 答案是,它比软件FMA快15%左右。这比我希望的要少得多。双重Mandelbrot计算需要4次双倍加法,4次双倍乘法(x*x
,y*y
,x*y
,和2*(x*y)
)。但是,2*(x*y)
multiplication is trivial for double-double因此可以在成本中忽略此乘法。因此,我认为使用硬件FMA的改进如此之小的原因是计算主要是慢速双倍加法(参见下面的装配)。
过去乘法比加法慢(并且程序员使用了几个技巧来避免乘法)但是对于Haswell来说,它似乎是另一种方式。不仅是因为FMA,还因为乘法可以使用两个端口但只添加一个。
所以我的问题(最后)是:
(x+y)*(x+y) - (x*x+y*y) = 2*x*y
使用另外两个加法来减少一次乘法。如果有人想知道双重方法比双重方法慢十倍。这并不是那么糟糕,我认为好像有一个硬件四精度类型,它可能至少是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
答案 0 :(得分:5)
为了回答我的第三个问题,我找到了一个更快的双倍加法解决方案。我在论文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| >= |bh|
。 Here is a discussion on how to implement SIMD minmag and maxmag functions。幸运的是,大多数附加代码不使用端口1.现在只有12条指令转到端口1而不是20。
以下是新Add22
的吞吐量分析表IACAThroughput 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
答案 1 :(得分:1)
为了加快算法的速度,我使用了基于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 | <阈值),仅每8次迭代就花费另一个fma和另一个乘法。
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
总结一下,
答案 2 :(得分:1)
您提到以下代码:
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
如果仔细检查,这些操作主要是依赖操作,并且不满足有关延迟/吞吐量效率的基本规则。大多数指令取决于前一个指令或之前的2个指令的结果。此序列包含30个周期的关键路径(约9或10条有关“ 3个周期的等待时间” /“ 1个周期的吞吐量”的指令)。
您的IACA在关键路径中报告“ CP” =>指令,并且评估的成本为20个周期的吞吐量。您应该获取延迟报告,因为如果您对执行速度感兴趣,那么它就很重要。
要消除此关键路径的开销,如果编译器无法执行此操作,则必须交织大约20条类似的指令(例如,因为您的double-double代码在单独的库中编译,而函数中无处没有-flto优化和vzeroupper进入和退出,vectorizer仅适用于内联代码)。
一种可能是并行运行2个计算(请参阅上一篇文章中的代码拼接以改善流水线化功能
如果我假设您的double-double代码看起来像这种“标准”实现
// (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端口使用率(还可以最大化每个周期的指令)。 / p>
您可以使用perf实用程序(软件包linux-tools-generic和linux-cloud-tools-generic)跟踪优化影响,以获取执行的指令数和每个周期的指令数。