x86-64的[g0带有-O3 -mavx -mtune=haswell
的gcc 5.3使surprisingly bulky code处理诸如以下代码的可能未对齐的输入:
// 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 bug,并提出了gcc在剥离未对齐的迭代时可以使用的更小/更好的代码的建议。不过,它可能仍然不是最佳选择。
这个问题是关于使用AVX实际是什么最佳]。我在问gcc和其他编译器可以/应该使用的一般情况的解决方案。 (我没有找到有关此问题的讨论的gcc邮件列表命中,但没有花很长时间。)
可能会有多个答案,因为对-mtune=haswell
的最佳选择可能与对-mtune=bdver3
(压路机)的最佳选择不同。然后是一个问题,当允许指令集扩展时(例如,AVX2用于256b整数填充,BMI1用于在更少的指令中将计数转换为位掩码)是最佳的。
我知道Agner Fog的《优化装配》指南,第13.5节,访问未对齐的数据和局部矢量。他建议要么使用未对齐的访问,在开始和/或结束处进行重叠的写操作,要么从对齐的访问中整理数据(但PALIGNR
仅占用imm8计数,所以2xpshufb
/ por
)。他认为VMASKMOVPS
没用,可能是因为它在AMD上的表现很差。我怀疑如果针对英特尔进行调优,则值得考虑。如何生成正确的遮罩并因此产生问题标题尚不明显。
可能会发现,最好像clang一样简单地使用未对齐的访问。对于短缓冲区,对齐的开销可能会扼杀避免在主循环中避免缓存行拆分的任何好处。对于大缓冲区,作为瓶颈的主内存或L3可能会隐藏高速缓存行拆分的代价。如果有人拥有实验数据来支持他们调整过的任何真实代码,那也是有用的信息。
VMASKMOVPS
确实对英特尔目标有用。 (SSE版本太可怕了,带有隐式的非时间提示,但AVX版本没有该提示。甚至还有一个新的内在函数可以确保您不会获得128b操作数的SSE版本:_mm128_maskstore_ps
) AVX版本为only a little bit slow on Haswell:
- 3微秒/ 4c延迟/每2c 1个吞吐量作为负载。
- 4 uops / 14c延迟/ 1 /每2c吞吐量作为256b存储。
- 4微码/ 13c延迟/每1c 1个吞吐量(作为128b存储。] >>
[在Jaguar(每22c tput 1个)和Bulldozer系列上的AMD CPU上,存储形式仍然非常缓慢,在Steamroller(与Bulldozer相似)上每16c 1个,或者在PILEDRIVER上每180c吞吐1个]]
但是如果我们确实想使用VMASKMOVPS
,则需要一个在每个元素中设置高位的向量,该向量应实际加载/存储。 PALIGNR和PSRLDQ(用于全1的向量)仅采用编译时常数计数。
请注意,其他位无所谓:不必全为1,因此有可能将一些设置位散布到元素的高位。
用于x86-64的带有-O3 -mavx -mtune = haswell的gcc 5.3使代码出奇的庞大,无法处理代码的可能未对齐的输入,例如://编译器输入的便捷简单示例//我不是...
Agner Fog的获取pshufb掩码的方法实际上提供了一个非常有效的想法:执行未对齐的加载以从表中获取数据窗口。与其使用庞大的掩码表,不如使用索引来对内存中的数据进行字节移位。
掩码以LSB-第一个字节顺序(因为它们存储在内存中),而不是向量中
{X3,X2,X1,X0}
元素的常用符号。如所写,它们与一个对齐的窗口对齐,包括内存中输入数组的开始/结束。
vpmosvsxbd指令即可获得掩码。 (ymm,mem形式不能微熔,所以是2 oups)。这需要AVX2。
没有AVX2:vmovdqu来自DWORD(DD
)而不是字节(DB
)的60B表。实际上,这将相对于AVX2 - save一个insn:
address & 0x1c
是索引,而无需向右移动两位。整个表仍然适合高速缓存行,但是算法没有空间容纳其他常量。
或:将2x vpmovsxbd放入两个128b寄存器([masktable_intro + rax]
和[masktable_intro + rax + 4]
)- vinsertf128
或者:(更多的insns和更多的shuffle-port压力,但是更少的load-port压力,几乎可以肯定会更糟)vpmovsxbw进入128b寄存器- 将vpunpcklwd / vpunpckhwd分为两个xmm regs(两者均为src1 = src2)
- vinsertf128
开销:- Integer ops:在开始时5 oups,以获取索引并对齐开始指针。 7 oups以获得端罩的索引。如果循环元素计数是向量宽度的倍数,则不仅仅使用未对齐,总共还有12个GP寄存器。
- AVX2:两个2融合域uop向量insns,从GP reg中的[0..7]索引转到YMM reg中的掩码。 (一个用于开始蒙版,一个用于结束蒙版)。使用24B表,该表在8B窗口中以字节粒度访问。
AVX:六个1个融合域uop向量insns(开头三个,结尾三个)。使用针对该表的RIP相对寻址,这些指令中的四个将为[base+index]
,并且不会微熔丝,因此额外的两个整数insns可能会更好。循环内的代码被复制3次。
[TODO:写另一个答案,动态生成掩码,可能是64b reg中的字节,然后将其解压缩为256b。也许是移位或BMI2的BZHI(-1,count)?
仅AVX:开始/结束时未对齐的访问,对负载进行流水线处理,以避免在原地重写时出现问题。
感谢@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]
vaddps ymm0, ymm0, ymm0 ; *= 2.0
; don't store yet
lea rax, [rdi+32]
and rax, ~0x1c ; 0x1c = 7 << 2 = 0b11100
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)
vaddss 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个额外整数,总计(以设置对齐起始位置)。我们已经在普通循环结构中使用了结束指针,所以这是免费的。
2个额外的循环体副本(加载/计算/存储)。 (剥离了第一个和最后一个迭代)。
当自动向量化时,编译器可能不会对发出这样的代码感到不满意。 Valgrind will report accesses outside of array bounds,并通过单步执行和解码指令来查看它们正在访问的内容。因此,仅停留在与数组中的最后一个元素相同的页面(和缓存行)中是不够的。还请注意,如果输入指针未与4B对齐,则可能会读入另一个页面并出现段错误。为了使Valgrind满意,我们可以尽早停止循环
two向量宽度,以特殊情况加载数组未对齐的最后一个向量宽度。这将需要额外的时间来复制循环主体(在此示例中是微不足道的,但故意这样做是微不足道的。)或者通过将介绍代码跳入循环的中间来避免流水线操作。 (尽管对于uop缓存而言,这可能不是最优的:(部分)循环体可能会在uop缓存中两次出现。)
TODO:编写一个跳入循环中间的版本。