【问题标题】:AVX2 byte gather with uint16 indices, into a __m256i使用 uint16 索引的 AVX2 字节收集到 __m256i
【发布时间】:2025-12-11 18:25:01
【问题描述】:

我正在尝试将 __m256i 变量与数组中的 32 个字符打包并由索引指定。 这是我的代码:

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]])

这个函数会被多次调用,使用相同的偏移量和不同的数组。但根据我的测试,我认为这不是最佳的。有什么改进的办法吗?

【问题讨论】:

  • 所以你需要做一个字节收集?或者你只需​​要一个字节洗牌? offset[]array[] 是什么类型? uint8_t array[]? offset[]array[] 是编译时常量吗?
  • @Peter,一个字节收集。 offset[] 和 array[] 的类型分别是 uint16_t 和 char。它们都不是编译时常量。
  • @Peter,由 gcc 版本 4.9.2 (GCC) 编译,带有选项 -mavx2 -O2。 cpu 信息:“Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz”,没有 avx512 标志。大约 100 多个数组的偏移量相同,但不是编译时常量。也许我们可以利用这个,但我对avx2不太熟悉
  • 我敢打赌,即使是 100 次使用相同 offset 的调用,“愚蠢的”JIT 编译肯定可以工作,因为它可以将负载数量减少一半,因为您可以硬编码offset[...]。当然,您的 JIT 必须快速完成此操作:一个问题可能是您必须生成 8 或 32 字节偏移量,具体取决于。使用所有 32 字节偏移量可能会更快。当然,任何一种 JIT 都是一种非常极端的、不可移植的解决方案,所以只有当这真的很重要时才走这条路。
  • @BeeOnRope:有 1k 或 10k 次重复使用 offset[],可能值得调用像 LLVM 这样的通用 JIT 来生成 vmovd / vpinsrb 指令序列。但是只有 100 个,是的,你必须自己编写代码。不过,关于固定指令宽度的好点。 vpinsrb 具有 2 字节 VEX 和 [base + disp32] 寻址模式为 10 字节。您可以使用来自offset[] 的 xmm 加载、一对vpshufb 将偏移量对齐到 rel32 插槽并将其余部分归零,然后使用 vpor 从具有[rdi + strict dword 0] 寻址模式的指令模板中 JIT 这个模式。跨度>

标签: c intrinsics avx pack avx2


【解决方案1】:

让我们首先看一下适用于随每次调用而变化的一般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 的所有内容,并且只是用完寄存器,因此它开始溢出一些,然后在它只是读取大部分 16 位元素的地方进行狂欢offset,然后立即将它们(作为 64 位零扩展值)存储到堆栈中。本质上,它无缘无故地复制了大部分 offset 数组(零扩展至 64 位):稍后它会读取溢出的值,当然它可能只是从 offset 读取的。

同样可怕的代码在您使用的旧版4.9.2 以及最近的7.2 中都很明显。


iccclang 都没有任何此类问题 - 它们都生成几乎相同的非常合理的代码,只使用 movzx 从每个 offset 位置读取一次,然后使用带有内存的 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 位。似乎在您正在使用的现代 uarch 上,这会同时在 2 个读取端口和端口 5(随机播放)上以每个周期 1 个元素出现瓶颈。因此,这可能具有每 32 个周期约 1 的吞吐量,以及接近 32 个周期的延迟(主要依赖链是通过正在接收 pinsrb 的工作进行中的xmm 寄存器,但列出的延迟为该指令的内存源版本只有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 位元素使用几个额外指令,因此每个元素吞吐量应该接近 1 个周期2

它在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 上出现瓶颈以进行 shuffle。我们也可以在单个 256 位加载中加载整个聚集索引向量。不幸的是,最细粒度的vpgathervpgatherdd,它使用 32 位偏移量加载 8 个 32 位元素。所以我们需要 4 个这样的集合来获取所有 32 个字节元素,然后需要以某种方式混合得到的向量。

我们实际上可以通过交错和调整偏移量来避免组合结果数组的大部分成本,以便每个 32 位值中的“目标”字节实际上是其正确的最终位置。所以你最终得到了 4 个 256 位向量,每个向量都有 8 个字节,你想要的,在正确的位置,24 个字节你不想要。您可以将vpblendw 两对向量放在一起,然后将vpblendb 这些结果放在一起,总共 3 个端口 5 微指令(必须有更好的方法来减少这种情况?)。

把它们加在一起,我得到了类似的东西:

  • 4 movups 加载 4 个 vpgatherdd 索引寄存器(可吊装)
  • 4vpgatherdd
  • 2 vpblendw(4 个结果 -> 2)
  • 1 movups 加载vpblendb 掩码(可吊装)
  • 1 vpblendb(2 个结果 -> 1)

除了vpgatherdds 之外,它看起来大约有 9 个微指令,其中 3 个进入端口 5,因此该端口有 3 个周期瓶颈,如果没有瓶颈,则大约 2.25 个周期(因为 vpgatherdd 可能不会使用端口 5)。在 Broadwell 上,vpgather 系列比 Haswell 有了很大改进,但对于 vpgatherdd,每个元素仍然需要大约 0.9 个周期,所以那里大约有 29 个周期。所以我们马上回到我们开始的地方,大约 32 个周期。

不过,还是有希望的:

  • 每个元素 0.9 个周期用于大部分纯 vpgatherdd 活动。也许那时混合代码或多或少是免费的,我们大约有 29 个周期(实际上,movups 仍将与集合竞争)。
  • vpgatherdd 在 Skylake 中再次变得更好,每个元素大约 0.6 个周期,因此当您将硬件升级到 Skylake 时,此策略将开始显着帮助。 (并且使用 AVX512BW 的策略可能会稍微领先于 vpinsrb,其中与 k-register 掩码混合的字节是有效的,并且 vpgatherdd zmm 每个元素的收集吞吐量略高于 ymm (InstLatx64 ).)
  • 预处理使您有机会检查是否从array 读取重复元素。在这种情况下,您可能会减少聚集的数量。例如,如果offset 中只有一半的元素是唯一的,那么您只能进行两次聚集以收集 16 个元素,然后 pshufb 根据需要注册以复制元素。 “减少”必须更笼统,但实际上看起来并不更贵(而且可能更便宜),因为 pshufb 非常笼统地完成了大部分工作。

扩展最后一个想法:您将在运行时分派到一个例程,该例程知道如何根据需要多少元素进行 1、2、3 或 4 次收集。这是相当量化的,但是您始终可以在这些截止点之间以更细粒度的方式调度标量负载(或具有更大元素的聚集,这更快)。你会很快达到收益递减。

您甚至可以将其扩展到处理 nearby 元素 - 毕竟,您要抓取 4 个字节来获取一个字节:因此,如果这 3 个浪费的字节中的任何一个实际上是另一个使用的 offset 值,那么您几乎可以免费获得它。现在,这需要一个更通用的缩减阶段,但看起来pshufb 仍然会完成繁重的工作,并且大部分艰苦的工作都仅限于预处理。


1 这是少数 SSE/AVX 指令之一,其中指令的内存源形式比 reg-reg 形式更有效:reg-reg 形式需要 2端口 5 上的 uops 将其限制为每个周期 0.5 的吞吐量,并使其延迟为 2。显然,内存加载路径避免了端口 5 所需的洗牌/混合之一。vpbroadcastd/q 也是如此。

2 每个周期有两次加载和一次存储,这将非常接近最大理论性能的参差不齐的边缘:它最大化了 L1 操作吞吐量,这通常会导致打嗝:例如,可能没有任何空闲周期来接受来自 L2 的传入高速缓存行。

【讨论】:

  • 如果加载/存储 uop 吞吐量是一个瓶颈,您可以更广泛地加载 offsetmovzx / shift 来获取索引。不幸的是 BMI2 bextr 是 2 微秒,否则它会是完美的。例如mov eax, [offset+4]/movzx ecx, ax/(使用 rcx 作为索引)/shr eax, 16/(使用 rax 作为索引)。所以这是 1 个负载和 2 个整数 ALU 微指令,而不是 2 movzx 负载微指令。您可以将其扩展到 64 位负载。如果您将offset 声明为uint32_t * 并强制转换+ 移位加载结果,您应该从C 编译器中得到类似的东西。或者使用数组和 uint32 的联合。
  • 是的,我查看了 bextr,但正如您所指出的那样它太慢了(如果有一个使用内存操作数作为源并且是 1 个融合 uop 的即时版本会很好)。当我决定 vpgather 是这个想法的自然扩展时,我正在编写一个从偏移量和 ALU 进行 64 位加载以获取字节的版本。
  • 也值得考虑(但你不会让 gcc 或 clang 为你生成这个 asm):在插入向量之前使用整数加载来组装 dwords 或 qwords,以绕过 port5 瓶颈。假设 Broadwell 的部分 reg 是 the same as HSW/SKLmov al, [rdi + rcx] 是 1 个微融合 ALU uop(对于任何端口,IIRC),它合并到 RAX 中。 movzx eax, [rdi + rcx] / shl eax, 8 / mov al, [rdi + rdx] / shl eax, 8 / ... / vmovq xmm0, rax.
  • 是的,这就是我将“标量技巧”与上面的pinsrb 结合起来的意思。只是没有那么的空间:矢量方法已经在 ~3 微秒,所以假设你这样做了 16 个元素(16*3 = 48 微秒),你有 16 微秒和玩。如果我们将标量内容压缩到 4 个微指令中,这将需要 12 个周期(由于“免费”16 个微指令而节省了 4 个周期),而您处于 28 个周期。好吧,我们可能节省了两个周期,这不是什么!忽略两边的一些边缘效应(例如vmovq),因为它们似乎几乎抵消了。当然,一些 ALU 操作会窃取端口 5...
  • @PeterCordes - 非常有趣的结果。我将循环计数从 100k 更改为 100m,并立即重现了您的结果。由于迭代次数较少,它们都以 1.00 个周期运行,而在大约 1m 到 2m 次迭代时,64 位版本开始变得不稳定,通常以 1.12 运行,但也有许多其他值,例如 1.04 和 1.03。到 100m 时,它始终以 1.12 个周期运行。