我正在尝试从数组中打包一个带有32个字符的__m256i变量,并由indices指定。 这是我的代码:
char array[]; // different array every time.
uint16_t offset[32]; // same offset reused many times
_mm256_set_epi8(array[offset[0]], array[offset[1]], array[offset[2]], array[offset[3]], array[offset[4]], array[offset[5]], array[offset[6]], array[offset[7]],
array[offset[8]],array[offset[9]],array[offset[10]],array[offset[11]], array[offset[12]], array[offset[13]], array[offset[14]], array[offset[15]],
array[offset[16]],array[offset[17]], array[offset[18]], array[offset[19]], array[offset[20]], array[offset[21]], array[offset[22]], array[offset[23]],
array[offset[24]],array[offset[25]],array[offset[26]], array[offset[27]], array[offset[28]], array[offset[29]], array[offset[30]],array[offset[31]])
使用相同的偏移和不同的数组将多次调用此函数。但根据我的测试,我认为这不是最佳选择。有什么想法改进吗?
答案 0 :(得分:3)
让我们首先看一下适用于一般offset
的解决方案,这些解决方案随每次调用而变化(这将成为现有函数的一个简单解决方案),然后我们将会这样做看看我们是否可以利用用于多次调用的相同offset
数组(array
始终不同)。
一个很大的问题是gcc
(旧的或新的)只为当前的函数实现生成awful code:
lea r10, [rsp+8]
and rsp, -32
push QWORD PTR [r10-8]
push rbp
mov rbp, rsp
push r15
push r14
push r13
push r12
push r10
push rbx
sub rsp, 40
movzx eax, WORD PTR [rsi+40]
movzx r14d, WORD PTR [rsi+60]
movzx r12d, WORD PTR [rsi+56]
movzx ecx, WORD PTR [rsi+44]
movzx r15d, WORD PTR [rsi+62]
movzx r13d, WORD PTR [rsi+58]
mov QWORD PTR [rbp-56], rax
movzx eax, WORD PTR [rsi+38]
movzx ebx, WORD PTR [rsi+54]
movzx r11d, WORD PTR [rsi+52]
movzx r10d, WORD PTR [rsi+50]
movzx r9d, WORD PTR [rsi+48]
movzx r8d, WORD PTR [rsi+46]
mov QWORD PTR [rbp-64], rax
movzx eax, WORD PTR [rsi+36]
movzx edx, WORD PTR [rsi+42]
mov QWORD PTR [rbp-72], rax
movzx eax, WORD PTR [rsi+34]
mov QWORD PTR [rbp-80], rax
movzx eax, WORD PTR [rsi+32]
mov QWORD PTR [rbp-88], rax
movzx eax, WORD PTR [rsi+30]
movzx r15d, BYTE PTR [rdi+r15]
mov QWORD PTR [rbp-96], rax
movzx eax, WORD PTR [rsi+28]
vmovd xmm2, r15d
vpinsrb xmm2, xmm2, BYTE PTR [rdi+r14], 1
mov QWORD PTR [rbp-104], rax
movzx eax, WORD PTR [rsi+26]
mov QWORD PTR [rbp-112], rax
movzx eax, WORD PTR [rsi+24]
mov QWORD PTR [rbp-120], rax
movzx eax, WORD PTR [rsi+22]
mov QWORD PTR [rbp-128], rax
movzx eax, WORD PTR [rsi+20]
mov QWORD PTR [rbp-136], rax
movzx eax, WORD PTR [rsi+18]
mov QWORD PTR [rbp-144], rax
movzx eax, WORD PTR [rsi+16]
mov QWORD PTR [rbp-152], rax
movzx eax, WORD PTR [rsi+14]
mov QWORD PTR [rbp-160], rax
movzx eax, WORD PTR [rsi+12]
mov QWORD PTR [rbp-168], rax
movzx eax, WORD PTR [rsi+10]
mov QWORD PTR [rbp-176], rax
movzx eax, WORD PTR [rsi+8]
mov QWORD PTR [rbp-184], rax
movzx eax, WORD PTR [rsi+6]
mov QWORD PTR [rbp-192], rax
movzx eax, WORD PTR [rsi+4]
mov QWORD PTR [rbp-200], rax
movzx eax, WORD PTR [rsi+2]
movzx esi, WORD PTR [rsi]
movzx r13d, BYTE PTR [rdi+r13]
movzx r8d, BYTE PTR [rdi+r8]
movzx edx, BYTE PTR [rdi+rdx]
movzx ebx, BYTE PTR [rdi+rbx]
movzx r10d, BYTE PTR [rdi+r10]
vmovd xmm7, r13d
vmovd xmm1, r8d
vpinsrb xmm1, xmm1, BYTE PTR [rdi+rcx], 1
mov rcx, QWORD PTR [rbp-56]
vmovd xmm5, edx
vmovd xmm3, ebx
mov rbx, QWORD PTR [rbp-72]
vmovd xmm6, r10d
vpinsrb xmm7, xmm7, BYTE PTR [rdi+r12], 1
vpinsrb xmm5, xmm5, BYTE PTR [rdi+rcx], 1
mov rcx, QWORD PTR [rbp-64]
vpinsrb xmm6, xmm6, BYTE PTR [rdi+r9], 1
vpinsrb xmm3, xmm3, BYTE PTR [rdi+r11], 1
vpunpcklwd xmm2, xmm2, xmm7
movzx edx, BYTE PTR [rdi+rcx]
mov rcx, QWORD PTR [rbp-80]
vpunpcklwd xmm1, xmm1, xmm5
vpunpcklwd xmm3, xmm3, xmm6
vmovd xmm0, edx
movzx edx, BYTE PTR [rdi+rcx]
mov rcx, QWORD PTR [rbp-96]
vpunpckldq xmm2, xmm2, xmm3
vpinsrb xmm0, xmm0, BYTE PTR [rdi+rbx], 1
mov rbx, QWORD PTR [rbp-88]
vmovd xmm4, edx
movzx edx, BYTE PTR [rdi+rcx]
mov rcx, QWORD PTR [rbp-112]
vpinsrb xmm4, xmm4, BYTE PTR [rdi+rbx], 1
mov rbx, QWORD PTR [rbp-104]
vpunpcklwd xmm0, xmm0, xmm4
vpunpckldq xmm0, xmm1, xmm0
vmovd xmm1, edx
movzx edx, BYTE PTR [rdi+rcx]
vpinsrb xmm1, xmm1, BYTE PTR [rdi+rbx], 1
mov rcx, QWORD PTR [rbp-128]
mov rbx, QWORD PTR [rbp-120]
vpunpcklqdq xmm0, xmm2, xmm0
vmovd xmm8, edx
movzx edx, BYTE PTR [rdi+rcx]
vpinsrb xmm8, xmm8, BYTE PTR [rdi+rbx], 1
mov rcx, QWORD PTR [rbp-144]
mov rbx, QWORD PTR [rbp-136]
vmovd xmm4, edx
vpunpcklwd xmm1, xmm1, xmm8
vpinsrb xmm4, xmm4, BYTE PTR [rdi+rbx], 1
movzx edx, BYTE PTR [rdi+rcx]
mov rbx, QWORD PTR [rbp-152]
mov rcx, QWORD PTR [rbp-160]
vmovd xmm7, edx
movzx eax, BYTE PTR [rdi+rax]
movzx edx, BYTE PTR [rdi+rcx]
vpinsrb xmm7, xmm7, BYTE PTR [rdi+rbx], 1
mov rcx, QWORD PTR [rbp-176]
mov rbx, QWORD PTR [rbp-168]
vmovd xmm5, eax
vmovd xmm2, edx
vpinsrb xmm5, xmm5, BYTE PTR [rdi+rsi], 1
vpunpcklwd xmm4, xmm4, xmm7
movzx edx, BYTE PTR [rdi+rcx]
vpinsrb xmm2, xmm2, BYTE PTR [rdi+rbx], 1
vpunpckldq xmm1, xmm1, xmm4
mov rbx, QWORD PTR [rbp-184]
mov rcx, QWORD PTR [rbp-192]
vmovd xmm6, edx
movzx edx, BYTE PTR [rdi+rcx]
vpinsrb xmm6, xmm6, BYTE PTR [rdi+rbx], 1
mov rbx, QWORD PTR [rbp-200]
vmovd xmm3, edx
vpunpcklwd xmm2, xmm2, xmm6
vpinsrb xmm3, xmm3, BYTE PTR [rdi+rbx], 1
add rsp, 40
vpunpcklwd xmm3, xmm3, xmm5
vpunpckldq xmm2, xmm2, xmm3
pop rbx
pop r10
vpunpcklqdq xmm1, xmm1, xmm2
pop r12
pop r13
vinserti128 ymm0, ymm0, xmm1, 0x1
pop r14
pop r15
pop rbp
lea rsp, [r10-8]
ret
基本上它试图预先对offset
进行所有读取,并且只是用完了寄存器,所以它开始溢出一些,然后继续溢出它的溢出的地方。只读取offset
的大多数16位元素,然后立即将它们(作为64位零扩展值)立即存储到堆栈中。基本上,它将大多数offset
数组(没有扩展为64位)复制到任何目的:它稍后读取溢出值,当然只能从offset
读取。
在您使用的旧4.9.2
版本以及最近的7.2
版本中,同样可怕的代码也很明显。
icc
和clang
都没有任何此类问题 - 它们都生成几乎相同的非常合理的代码,只需使用offset
从每个movzx
位置读取一次,然后插入使用vpinsrb
的字节,其内存源操作数基于刚刚读取的offset
:
gather256(char*, unsigned short*): # @gather256(char*, unsigned short*)
movzx eax, word ptr [rsi + 30]
movzx eax, byte ptr [rdi + rax]
vmovd xmm0, eax
movzx eax, word ptr [rsi + 28]
vpinsrb xmm0, xmm0, byte ptr [rdi + rax], 1
movzx eax, word ptr [rsi + 26]
vpinsrb xmm0, xmm0, byte ptr [rdi + rax], 2
movzx eax, word ptr [rsi + 24]
...
vpinsrb xmm0, xmm0, byte ptr [rdi + rax], 14
movzx eax, word ptr [rsi]
vpinsrb xmm0, xmm0, byte ptr [rdi + rax], 15
movzx eax, word ptr [rsi + 62]
movzx eax, byte ptr [rdi + rax]
vmovd xmm1, eax
movzx eax, word ptr [rsi + 60]
vpinsrb xmm1, xmm1, byte ptr [rdi + rax], 1
movzx eax, word ptr [rsi + 58]
vpinsrb xmm1, xmm1, byte ptr [rdi + rax], 2
movzx eax, word ptr [rsi + 56]
vpinsrb xmm1, xmm1, byte ptr [rdi + rax], 3
movzx eax, word ptr [rsi + 54]
vpinsrb xmm1, xmm1, byte ptr [rdi + rax], 4
movzx eax, word ptr [rsi + 52]
...
movzx eax, word ptr [rsi + 32]
vpinsrb xmm1, xmm1, byte ptr [rdi + rax], 15
vinserti128 ymm0, ymm1, xmm0, 1
ret
非常好。 vinserti128
两个xmm
向量一起存在少量额外开销,每个向量都有一半结果,显然是因为vpinserb
无法写入高128位。看起来像你正在使用的现代uarchs同时会在2个读取端口和每个周期1个元素的端口5(shuffle)上出现瓶颈。因此,这可能会有每32个周期大约1个的吞吐量,并且延迟接近32个周期(主要依赖链是通过正在进行的xmm
寄存器接收pinsrb
但是此指令的内存源版本列出的延迟仅为1个周期 1 。
我们能否在gcc上接近这32个性能?看来是这样。这是一种方法:
uint64_t gather64(char *array, uint16_t *offset) {
uint64_t ret;
char *p = (char *)&ret;
p[0] = array[offset[0]];
p[1] = array[offset[1]];
p[2] = array[offset[2]];
p[3] = array[offset[3]];
p[4] = array[offset[4]];
p[5] = array[offset[5]];
p[6] = array[offset[6]];
p[7] = array[offset[7]];
return ret;
}
__m256i gather256_gcc(char *array, uint16_t *offset) {
return _mm256_set_epi64x(
gather64(array, offset),
gather64(array + 8, offset + 8),
gather64(array + 16, offset + 16),
gather64(array + 24, offset + 24)
);
}
这里我们依靠堆栈上的临时数组一次从array
收集8个元素,然后我们将其用作_mm256_set_epi64x
的输入。总的来说,每8字节元素使用2个加载和1个存储,每个64位元素使用几个额外的指令,因此每个元素吞吐量 2 应该接近1个周期。
它会产生"预期"在gcc
中内联code:
gather256_gcc(char*, unsigned short*):
lea r10, [rsp+8]
and rsp, -32
push QWORD PTR [r10-8]
push rbp
mov rbp, rsp
push r10
movzx eax, WORD PTR [rsi+48]
movzx eax, BYTE PTR [rdi+24+rax]
mov BYTE PTR [rbp-24], al
movzx eax, WORD PTR [rsi+50]
movzx eax, BYTE PTR [rdi+24+rax]
mov BYTE PTR [rbp-23], al
movzx eax, WORD PTR [rsi+52]
movzx eax, BYTE PTR [rdi+24+rax]
mov BYTE PTR [rbp-22], al
...
movzx eax, WORD PTR [rsi+62]
movzx eax, BYTE PTR [rdi+24+rax]
mov BYTE PTR [rbp-17], al
movzx eax, WORD PTR [rsi+32]
vmovq xmm0, QWORD PTR [rbp-24]
movzx eax, BYTE PTR [rdi+16+rax]
movzx edx, WORD PTR [rsi+16]
mov BYTE PTR [rbp-24], al
movzx eax, WORD PTR [rsi+34]
movzx edx, BYTE PTR [rdi+8+rdx]
movzx eax, BYTE PTR [rdi+16+rax]
mov BYTE PTR [rbp-23], al
...
movzx eax, WORD PTR [rsi+46]
movzx eax, BYTE PTR [rdi+16+rax]
mov BYTE PTR [rbp-17], al
mov rax, QWORD PTR [rbp-24]
mov BYTE PTR [rbp-24], dl
movzx edx, WORD PTR [rsi+18]
vpinsrq xmm0, xmm0, rax, 1
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-23], dl
movzx edx, WORD PTR [rsi+20]
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-22], dl
movzx edx, WORD PTR [rsi+22]
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-21], dl
movzx edx, WORD PTR [rsi+24]
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-20], dl
movzx edx, WORD PTR [rsi+26]
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-19], dl
movzx edx, WORD PTR [rsi+28]
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-18], dl
movzx edx, WORD PTR [rsi+30]
movzx edx, BYTE PTR [rdi+8+rdx]
mov BYTE PTR [rbp-17], dl
movzx edx, WORD PTR [rsi]
vmovq xmm1, QWORD PTR [rbp-24]
movzx edx, BYTE PTR [rdi+rdx]
mov BYTE PTR [rbp-24], dl
movzx edx, WORD PTR [rsi+2]
movzx edx, BYTE PTR [rdi+rdx]
mov BYTE PTR [rbp-23], dl
movzx edx, WORD PTR [rsi+4]
movzx edx, BYTE PTR [rdi+rdx]
mov BYTE PTR [rbp-22], dl
...
movzx edx, WORD PTR [rsi+12]
movzx edx, BYTE PTR [rdi+rdx]
mov BYTE PTR [rbp-18], dl
movzx edx, WORD PTR [rsi+14]
movzx edx, BYTE PTR [rdi+rdx]
mov BYTE PTR [rbp-17], dl
vpinsrq xmm1, xmm1, QWORD PTR [rbp-24], 1
vinserti128 ymm0, ymm0, xmm1, 0x1
pop r10
pop rbp
lea rsp, [r10-8]
ret
这种方法在尝试读取堆栈缓冲区时将遭受4次(非依赖性)存储转发停顿,这将使延迟稍微差于32个周期,可能在40年代中期(如果你假设它的话)最后一个不会被隐藏的摊位。您也可以删除gather64
函数并在32字节缓冲区中展开整个内容,最后只需一次加载。这导致只有一个停顿,并且摆脱了将每个64位值一次加载到结果中的小开销,但总体效果可能更差,因为较大的负载似乎有时会遭受更大的转发停顿。
我很确定你可以选择更好的方法。例如,你可以写出"长手"在内在函数中,clang和icc使用的vpinsrb
方法。这很简单,gcc
应该正确。
如果对多个不同的offset
输入重复使用array
数组怎么办?
我们可以看一下预处理offset
数组,以便我们的核心加载循环更快。
一种可行的方法是使用vgatherdd
有效地加载元素而不会在端口5上出现瓶颈以进行混洗。我们也可以在单个256位加载中加载整个聚集索引向量。不幸的是,最细粒度的vpgather
是vpgatherdd
,它使用32位偏移加载8个32位元素。所以我们需要4个这样的集合获得所有32个字节元素,然后需要以某种方式混合生成的向量。
我们实际上可以避免通过交错和调整偏移来组合结果数组的大部分成本,以便" target"每个32位值中的字节实际上是其正确的最终位置。因此,您最终得到4个256位向量,每个向量具有您想要的8个字节,位于正确的位置,以及您不想要的24个字节。您可以vpblendw
两对向量一起,然后vpblendb
这些结果一起,总共3个端口5 uops(那里有更好的方法来进行这种减少?)
将它们全部加在一起,我得到类似的东西:
movups
加载4个vpgatherdd索引regs(可以悬挂)vpgatherdd
vpblendw
(4个结果 - > 2)movups
加载vpblendb
掩码(可以悬挂)vpblendb
(2个结果 - > 1)除了vpgatherdd
之外,它看起来大约有9个uop,其中3个进入端口5,因此在该端口上有3个循环瓶颈,或者如果没有瓶颈则约为2.25个循环(因为{{1}可能不会使用端口5)。在Broadwell上,vpgatherdd
家族比Haswell有了很大改进,但vpgather
每个元素仍然需要大约0.9个周期,所以在那里大约有29个周期。所以我们回到我们开始的地方,大约32个周期。
仍有一些希望:
vpgatherdd
个活动。也许然后混合代码或多或少是免费的,我们大约有29个周期(实际上,vpgatherdd
仍将与聚集竞争)。 movups
在Skylake中再次变得更好,每个元素大约0.6个周期,因此当您将硬件升级到Skylake时,此策略将开始显着提升。 (并且该策略可能会在使用AVX512BW的vpgatherdd
之前稍微拉一点,其中具有vpinsrb
寄存器掩码的字节混合是有效的,并且每个元素的k
收集吞吐量略高于vpgatherdd zmm
{1}}(InstLatx64)。)ymm
读取重复元素。在这种情况下,您可以减少收集的数量。例如,如果array
中只有一半的元素是唯一的,那么您只能进行两次收集来收集16个元素,然后offset
注册以根据需要复制元素。 "减少"必须更一般,但它实际上看起来并不昂贵(并且可能更便宜),因为pshufb
在大部分工作中都很普遍。扩展最后一个想法:您将在运行时调度到一个知道如何进行1,2,3或4次收集的例程,具体取决于需要多少元素。这是相当量化的,但是你可以总是以更细粒度的方式使用标量负载(或者使用更大元素的集合,这些更快)在这些截止点之间进行调度。你很快就会收益递减。
你甚至可以扩展它来处理附近的元素 - 毕竟,你抓住4个字节来获取一个字节:所以如果这3个浪费的字节中的任何一个实际上是另一个使用的{{1}价值,然后你几乎免费得到它。现在,这需要一个更普遍的减少阶段,但似乎pshufb
仍然会做繁重的工作,大部分的努力仅限于预处理。
1 这是一些SSE / AVX指令之一,其中指令的存储器源形式比reg-reg形式更有效:reg-reg形式需要2 uops在端口5上限制为每个周期0.5的吞吐量,并给它一个2的延迟。显然,内存加载路径避免了端口5上需要的一个混洗/混合。offset
也是这样的。
2 每个周期有两个负载和一个存储,这将运行得非常接近最大理论性能的不规则边缘:它最大限度地提高了L1运行吞吐量导致打嗝:例如,可能没有任何备用周期来接受来自L2的传入缓存行。