考虑x86中的以下循环:
; on entry, rdi has the number of iterations
.top:
; some magic happens here to calculate a result in rax
mov [array + rdi * 8], rax ; store result in output array
dec rdi
jnz .top
这很简单:有些东西在rax
中计算结果(未显示)然后我们将结果存储到数组中,与我们用rdi
索引时的顺序相反。
我想转换上面的循环而不对内存进行任何写入(我们可以假设未显示的计算不会写入内存)。
只要rdi
中的循环计数有限,我就可以使用ymm
regs提供的充足空间(512字节)来保存值,但实际上看起来很尴尬这个,因为你不能“索引”一个任意的寄存器。
一种方法是始终将ymm
寄存器的整个“数组”按一个元素混洗,然后将元素插入新释放的位置。
这样的事情:
vpermq ymm3, ymm3, 10_01_00_11b ; left rotate ymm by qword
vpermq ymm2, ymm2, 10_01_00_11b ; left rotate ymm by qword
vpermq ymm1, ymm1, 10_01_00_11b ; left rotate ymm by qword
vpermq ymm0, ymm0, 10_01_00_11b ; left rotate ymm by qword
vblenddd ymm3, ymm3, ymm2, 3 ; promote one qword of ymm2 to ymm3
vblenddd ymm2, ymm2, ymm1, 3 ; promote one qword of ymm1 to ymm2
vblenddd ymm1, ymm1, ymm0, 3 ; promote one qword of ymm0 to ymm1
pinsrq xmm0, rax, 0 ; playing with mixed-VEX mode fire (see Peter's answer)
这显示只处理16个寄存器中的4个,所以显然要做16个,这将是很多代码(32个指令)。
有更好的方法吗?
不可预测的分支是不可取的,但我们仍然可以考虑使用它们的解决方案。
答案 0 :(得分:3)
你不能vpinsrq
进入YMM注册。只有xmm目的地可用,所以它不可避免地将整个YMM寄存器的上部通道归零。它与AVX1一起作为128位指令的VEX版本引入。 AVX2和AVX512未将其升级到YMM / ZMM目的地。我猜他们不想在高通道中提供插入,提供仍然只看到imm8的最低位的YMM版本会很奇怪。
您将需要一个临时寄存器,然后与vpblendd
混合成YMM。 或(在Skylake或AMD上)使用legacy-SSE版本保持高位字节不变!在Skylake上,使用legacy-SSE指令写入XMM reg对整个寄存器具有错误的依赖性。您想要这种错误依赖。 (我没有测试过这个;它可能触发某种合并的uop)。但是你不希望哈斯韦尔把它放在所有YMM regs的上半部分,进入“状态C”。
显而易见的解决方案是给自己留下一个临时注册表,用于vmovq
+ vpblendd
(而不是vpinsrq y,r,0
)。这仍然是2 uop,但vpblendd
在Intel CPU上不需要端口5,以防万一。 (movq uses port 5). If you're really hard up for space, the
mm0..7` MMX寄存器可用。
使用嵌套循环,我们可以拆分工作。通过少量展开内循环,我们可以大部分消除这部分成本。
例如,如果我们有一个内部循环产生4个结果,我们可以在内部循环中的2个或4个寄存器上使用您的强制堆栈方法,从而产生适度的开销而没有实际的展开(“魔术”有效负载仅出现一次)。 3或4个uop,任选地没有环携带的dep链。
; on entry, rdi has the number of iterations
.outer:
mov r15d, 3
.inner:
; some magic happens here to calculate a result in rax
%if AVOID_SHUFFLES
vmovdqa xmm3, xmm2
vmovdqa xmm2, xmm1
vmovdqa xmm1, xmm0
vmovq xmm0, rax
%else
vpunpcklqdq xmm2, xmm1, xmm2 ; { high=xmm2[0], low=xmm1[0] }
vmovdqa xmm1, xmm0
vmovq xmm0, rax
%endif
dec r15d
jnz .inner
;; Big block only runs once per 4 iters of the inner loop, and is only ~12 insns.
vmovdqa ymm15, ymm14
vmovdqa ymm13, ymm12
...
;; shuffle the new 4 elements into the lowest reg we read here (ymm3 or ymm4)
%if AVOID_SHUFFLES ; inputs are in low element of xmm0..3
vpunpcklqdq xmm1, xmm1, xmm0 ; don't write xmm0..2: longer false dep chain next iter. Or break it.
vpunpcklqdq xmm4, xmm3, xmm2
vinserti128 ymm4, ymm1, xmm4, 1 ; older values go in the top half
vpxor xmm1, xmm1, xmm1 ; shorten false-dep chains
%else ; inputs are in xmm2[1,0], xmm1[0], and xmm0[0]
vpunpcklqdq xmm3, xmm0, xmm1 ; [ 2nd-newest, newest ]
vinserti128 ymm3, ymm2, xmm3, 1
vpxor xmm2, xmm2,xmm2 ; break loop-carried dep chain for the next iter
vpxor xmm1, xmm1,xmm1 ; and this, which feeds into the loop-carried chain
%endif
sub rdi, 4
ja .outer
奖励:这只需要AVX1(并且在AMD上更便宜,保持256位向量不在内循环中)。我们仍然得到12 x 4 qwords的存储而不是16 x 4.这无论如何都是一个任意数字。
我们可以像这样展开内循环:
.top:
vmovdqa ymm15, ymm14
...
vmovdqa ymm3, ymm2 ; 12x movdqa
vinserti128 ymm2, ymm0, xmm1, 1
magic
vmovq xmm0, rax
magic
vpinsrq xmm0, rax, 1
magic
vmovq xmm1, rax
magic
vpinsrq xmm1, rax, 1
sub rdi, 4
ja .top
当我们离开循环时,ymm15..2 和 xmm1和0充满了有价值的数据。如果它们位于底部,它们将运行相同的次数,但是ymm2将是xmm0和1的副本。jmp
进入循环而不在第一个iter上执行vmovdqa
内容是一种选择。
每4x magic
,端口5(movq + pinsrq),12 vmovdqa
(无执行单元)和1x vinserti128(端口5)再花费6 uop。因此,每4 magic
为19 uops,或4.75 uops。
您可以将vmovdqa
+ vinsert
与第一个magic
交错,或者在第一个magic
之前/之后将其拆分。你不能在vinserti128
之后破解xmm0,但是如果你有一个备用整数reg,你可以延迟vmovq
。
另一个循环嵌套级别或其他展开会大大减少vmovdqa
条指令的数量。但是,将数据拖入YMM注册表只需要最低成本。 Loading an xmm from GP regs
AVX512可以给我们更便宜的int-> xmm。 (并且它允许写入YMM的所有4个元素)。但我没有看到它避免需要展开或嵌套循环以避免每次都触及所有寄存器。
PS:
我对shuffle累加器的第一个想法是将元素向左移动。但后来我意识到这最终得到了5个状态元素,而不是4个元素,因为我们在两个寄存器中有高低,再加上新写的xmm0。 (并且可以使用vpalignr。)
留下这里作为你可以用vshufpd
做什么的一个例子:在一个寄存器中从低到高移动,在另一个寄存器中从高处合并为新低。
vshufpd xmm2, xmm1,xmm2, 01b ; xmm2[1]=xmm2[0], xmm2[0]=xmm1[1]. i.e. [ low(xmm2), high(xmm1) ]
vshufpd xmm1, xmm0,xmm1, 01b
vmovq xmm0, rax
对于将向量寄存器写入存储器的一般情况,我们可以vpbroadcastq zmm0{k1}, rax
并使用不同的zmm
掩码重复其他k1
寄存器。具有合并屏蔽的广播(其中屏蔽具有单个位设置)为我们提供了向量寄存器的索引存储,但是我们需要为每个可能的目标寄存器提供一条指令。
制作面具:
xor edx, edx
bts rdx, rcx # rdx = 1<<(rcx&63)
kmovq k1, rdx
kshiftrq k2, k1, 8
kshiftrq k3, k1, 16
...
从ZMM寄存器读取:
vpcompressq zmm0{k1}{z}, zmm1 ; zero-masking: zeros whole reg if no bits set
vpcompressq zmm0{k2}, zmm2 ; merge-masking
... repeat as many times as you have possible source regs
vmovq rax, zmm0
(请参阅vpcompressq
的文档:使用零屏蔽,将所有元素归零所有元素。
要隐藏vpcompressq延迟,您可以将多个dep链转换为多个tmp向量,最后是vpor xmm0, xmm0, xmm1
。 (其中一个向量将全部为零,另一个将具有所选元素。)
在SKX上,它具有3c延迟和2c吞吐量according to this instatx64 report。
答案 1 :(得分:1)
您可以考虑一些选项:
如果您展开循环(由于ymm
寄存器中的可用存储空间限制为64个qwords而必须具有有限的迭代次数),您将有机会使用硬编码逻辑来插入结果rax
直接在正确的地方,例如。使用pinrsq
或movq
有时会与shuffle相结合,让您访问高通道。每次迭代可能只需要1.25条指令,比32条好得多!
您当前的改组解决方案可以表征为通过寄存器的水平旋转,从ymm N
的高qword进入ymm N+1
的低qword。也就是说,一个寄存器中的相邻元素在您的方案中逻辑上相邻。相反,您可以执行垂直旋转,而在给定qword
通道中的元素逻辑上与寄存器ymm N-1
和ymm N+1
中相同通道中的元素相邻。这避免了任何水平改组的需要,并且大多数移位仅需要单个寄存器寄存器mov
。您只需要对第一个和最后一个寄存器进行特殊处理,即将元素包装到下一个通道中。
这样的事情:
; shift all lanes "up"
vmovdqa ymm15, ymm3
vmovdqa ymm3, ymm2
vmovdqa ymm2, ymm1
vmovdqa ymm1, ymm0
; wrap from the top register back to ymm0, shifting to the left by 1
vpermq ymm0, ymm15, 10_01_00_11b
; store new element
vpinsrq ymm0, rax, 0
这就像你要获得一般性的一样简单&#34;转移每个元素&#34;策略:每个vmovdqa
寄存器使用一个ymm
,以及执行环绕和新元素插入的其他说明。就向量运算而言,寄存器寄存器移动比任何其他类型的操作快得多,因为它们可以移动消除(0延迟)并且每个周期可以执行4次。
这种方法确实需要一个临时注册(在上面的示例中为ymm15
),我无法想出消除它的简单方法,因此您最多可以使用15个注册为&#34;队列&#34;。
您可以根据迭代次数计算间接跳转到一个将元素放在正确位置的短(2-4指令)序列。基本上是一个vpinsrq
,在某些情况下还有一些额外的改组来进入高速公路。
这种类型的表可以是完全通用的,即允许以任何顺序写入任意索引,但如果您知道您按上述顺序索引,则可以使用该假设简化表(即,您可以处理通过首先写入低级元素然后使用vinserti128
或类似的东西将它们移动到正确的时间进入高通道的高通道。
这张表可能是第一次错误预测。之后,它可能会也可能不会,取决于间接分支预测器的模式和强度。