带有-O3 -mavx -mtune=haswell
x86-64的gcc 5.3产生了令人惊讶的庞大代码,以处理可能未对齐的代码输入,例如:
// convenient simple example of compiler input
// I'm not actually interested in this for any real program
void floatmul(float *a) {
for (int i=0; i<1024 ; i++)
a[i] *= 2;
}
clang使用未对齐的加载/存储指令,但gcc进行了标量引入/输出和对齐的矢量循环:它剥离了最初的多达7个未对齐的迭代,将其完全展开为一系列
vmovss xmm0, DWORD PTR [rdi]
vaddss xmm0, xmm0, xmm0 ; multiply by two
vmovss DWORD PTR [rdi], xmm0
cmp eax, 1
je .L13
vmovss xmm0, DWORD PTR [rdi+4]
vaddss xmm0, xmm0, xmm0
vmovss DWORD PTR [rdi+4], xmm0
cmp eax, 2
je .L14
...
尤其是,这看起来非常糟糕。对于具有uop缓存的CPU。我报告了一个有关gcc的错误,并建议使用gcc在剥离未对齐的迭代时可以使用的更小/更好的代码。不过,它可能仍然不是最佳的。
这个问题是关于AVX实际最佳选择。我在问gcc和其他编译器可以/应该使用的一般情况的解决方案。(我没有找到有关此问题的讨论的gcc邮件列表命中,但没有花很长时间。)
可能会有多个答案,因为最佳的选择-mtune=haswell
可能与-mtune=bdver3
(steamroller)的选择不同。然后是一个问题,当允许指令集扩展时(例如,用于256b整数填充的AVX2,用于在更少指令中将计数转换为位掩码的BMI1),最佳选择是什么。
我知道Agner Fog的《优化程序集》指南第13.5节,访问未对齐的数据和局部向量。他建议要么使用未对齐的访问,在开始和/或结束处执行重叠的写操作,要么从对齐的访问中整理数据(但PALIGNR
仅占用imm8的计数,因此为2x pshufb
/ por
)。他打折VMASKMOVPS
没有用,可能是因为它在AMD上的表现很差。我怀疑,如果要针对英特尔进行调整,则值得考虑。如何生成正确的遮罩(问题标题)尚不明显。
可能会发现,最好像clang一样简单地使用未对齐的访问。对于短缓冲区,对齐的开销可能会扼杀避免在主循环中避免高速缓存行拆分的任何好处。对于大缓冲区,作为瓶颈的主内存或L3可能会隐藏高速缓存行拆分的代价。如果有人拥有实验数据来支持他们调整过的任何真实代码,那也是有用的信息。
VMASKMOVPS
确实适用于英特尔目标。(SSE版本太可怕了,带有隐式的非时间提示,但AVX版本没有该提示。甚至还有一个新的内在函数可以确保您不会获得128b操作数的SSE版本:)_mm128_maskstore_ps
AVX版本是在Haswell上只有一点点慢:
在Jaguar(每22c tput 1个)和Bulldozer系列上的AMD CPU上,存储形式仍然非常缓慢:在Steamroller(与Bulldozer相似)上,每16c 1个,或者在PILEDRIVER上每180c吞吐量为1。
但是,如果确实要使用VMASKMOVPS
,则需要一个在每个元素中应实际加载/存储的高位向量。PALIGNR和PSRLDQ(用于全1的向量)仅采用编译时常数计数。
请注意,其他位无所谓:不必全为1,因此可能会将一些设置位分散到元素的高位。
感谢@StephenCanon指出,这比VMASKMOVPS
任何VMASKMOVPS
有助于循环未对齐缓冲区的方法要好。
尤其是期望编译器将其作为循环转换来做,这可能有点多。因为明显的方法会使Valgrind不高兴(请参阅下文)。
section .text
global floatmul ; (float *rdi)
floatmul:
lea rdx, [rdi + 4096 - 32] ; one full vector less than the end address (calculated *before* masking for alignment).
;; replace 4096 with rsi*4 if rsi has the count (in floats, not bytes)
vmovups ymm0, [rdi] ; first vector
vaddps ymm0, ymm0, ymm0 ; *= 2.0
; don't store yet
lea rax, [rdi+32]
and rax, ~0x1c ; 0x1c = 7 << 2 = 0b11100 ; clear those bits.
vmovups ymm1, [rax] ; first aligned vector, for use by first loop iteration
vmovups [rdi], ymm0 ; store the first unaligned vector
vmovups ymm0, [rdx] ; load the *last* unaligned vector
.loop:
;; on entry: [rax] is already loaded into ymm1
vaddps ymm1, ymm1, ymm1 ; *= 2.0
vmovups [rax] ; vmovaps would fault if p%4 != 0
add rax, 32
vmovups ymm1, [rax]
cmp rax, rdx ; while( (p+=8) < (endp-8) );
jb .loop
; discard ymm1. It includes data from beyond the end of the array (aligned case: same as ymm0)
vaddps ymm0, ymm0, ymm0 ; the last 32B, which we loaded before the loop
vmovups [rdx], ymm0
ret
; End alignment:
; a[] = XXXX XXXX ABCD E___ _ = garbage past the end
; ^rdx
; ^rax ^rax ^rax ^rax(loop exit)
; ymm0 = BCDE
; ymm1 loops over ..., XXXX, ABCD, E___
; The last load off the end of the array includes garbage
; because we pipeline the load for the next iteration
在循环开始时从数组末尾进行加载似乎有些怪异,但希望它不会混淆硬件预取器,也不会减慢从内存中流式传输数组的开始。
共2个额外的整数oups(用于设置对齐起始)。我们已经在正常循环结构中使用了结束指针,因此这是免费的。
循环主体的2个额外副本(加载/计算/存储)。(第一次和最后一次迭代被剥离)。
当自动向量化时,编译器可能不会对发出这样的代码感到不满意。Valgrind将报告超出数组范围的访问,并通过单步执行和解码指令来查看访问的内容。因此,仅停留在与数组中的最后一个元素相同的页面(和缓存行)中是不够的。还要注意,如果输入指针不是4B对齐的,我们可能会读入另一个页面并出现段错误。
为了使Valgrind满意,我们可以提前停止两个矢量宽度的循环,以特殊方式加载数组中未对齐的最后一个矢量宽度。这将需要额外的时间来复制循环主体(在此示例中是微不足道的,但是故意这样做是微不足道的。)或者通过将介绍代码跳转到循环的中间来避免流水线操作。(不过,对于uop缓存而言,这可能不是最优的:(部分)循环体可能在uop缓存中结束两次。)
TODO:编写一个跳入循环中间的版本。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句