【问题标题】:Find the first instance of a character using simd使用 simd 查找字符的第一个实例
【发布时间】:2017-04-16 08:42:27
【问题描述】:

我正在尝试查找字符的第一个实例,在本例中为 '"' 使用 simd(AVX2 或更早版本)。我想使用 _mm256_cmpeq_epi8,但是我需要一种快速的方法来查找是否有任何__m256i 中的结果字节已设置为 0xFF。然后计划使用 _mm256_movemask_epi8 将结果从字节转换为位,并使用 ffs 获得匹配索引。使用一次移动一部分是否更好_mm_movemask_epi8?还有其他建议吗?

【问题讨论】:

  • 我应该补充一点,simd 不是必需的,一般来说我只是在寻找最快的方法。也许有点魔法?
  • 你的基本想法是正确的——我感觉可能已经有一个 SIMD 实现,就像你在 StackOverflow 上一个问题中描述的那样,但是快速搜索并没有发现它。请注意,您正在实现的实际上是strchr(或memchr,如果您知道长度),并且很可能已经有可用的SIMD 优化实现。另请注意,对于尚未在缓存中的字符串,您的函数可能会受到内存带宽的限制。
  • Here's an SSE implementation which scans a string for a '\0'(实际上是strlen),你也许可以适应。

标签: x86 sse simd avx avx2


【解决方案1】:

_mm256_cmpeq_epi8 -> _mm256_movemask_epi8 的想法是正确的。 AFAIK,至少对于英特尔 CPU,这是实现此功能的最佳方式。 PMOVMSKB r32, ymm 与 XMM 16 字节版本的速度相同,因此将 256b 向量的两个通道解包并分别移动掩码然后重新组合整数结果将是一个巨大的损失。 (来源:Agner Fog's instruction table。在 标签维基中查看其他性能链接。)

ffs 保留到您从_mm256_movemask_epi8 识别出非零结果之后,使循环内的代码尽可能高效。

TEST/JCC 可以将宏融合到单个微指令中,但 BSF/JCC 不能,因此需要额外的指令。 (无论如何,你都很难让 C 编译器发出 BSF/JCC。更有可能对 ffs 的结果进行分支会给你某种输入非零的测试,然后是 BSF,然后加 1,然后比较和分支。与仅测试 movemask 结果相比,这显然是可怕的。)

另请注意,对于类似的问题,比较移动掩码(例如检查它是否为 0xFFFFFFFF)与在非零时进行分支一样有效。


正如 Paul R 所建议的,查看一些 strlen、strchr 和 memchr 实现可能会有所帮助。在开源 libc 实现和其他地方有多个手写 asm 实现。 (例如 glibc 和 Agner Fog's asmlib。)

glibc 的许多版本扫描到对齐边界,然后使用一次读取 64B 的展开循环(在 4 个 SSE 向量中,因为我认为 glibc 没有 AVX2 版本)。

要针对长字符串进行优化,请通过对比较结果进行 OR 运算来减少测试比较结果的开销,然后进行检查。如果您找到了命中,请返回并重新测试您的向量,看看哪个向量有命中。

在一个 64 位整数上执行 ffs 可能会更有效一些,该整数是由多个移动掩码结果(使用 shift 和 |)构建的。在测试为零之前,我不确定是否要在循环内执行此操作;我不记得 glibc 的 strlen 策略之一是否这样做了。


我在这里建议的所有内容都可以在 asm 中看到,用于 strlen、memchr 和相关函数的各种 glibc 策略。这是sysdeps/x86_64/strlen.S,但我可能有另一个源文件在某处使用超过基线 SSE2。 (或者不是,我可能正在考虑一个不同的功能,也许除了 SSE2 之外没有什么可得到的,直到 AVX(3 操作数 insns)和 AVX2(256b 整数向量)。

另见:

  • glibc 的strchr-avx2.S(Woboq.org 有一个很好的源代码浏览器,可以搜索文件名/符号)。
  • glibc 的memchr-avx2.S

glibc's memchr 使用 PMAXUB 而不是 POR。我不确定这对于某些神秘的微架构原因是否有用,但它在大多数 CPU 上的较少端口上运行。也许这是需要的,以避免与其他东西发生资源冲突? IDK,看起来很奇怪,因为它与 PCMPEQB 竞争。

【讨论】:

  • _mm_movemask_epi8 背后的想法是,它在新的处理器上看起来比 _mm256_movemask_epi8 更快,即使它需要被调用两次。如果没有,那么您可以节省避免额外电话的费用。这当然似乎取决于处理器,所以在 Haswell 上它们具有相同的延迟,更大的调用(即 _mm256_movemask_epi8)似乎是一种更好的方法。
  • @Jimbo:哦,嗯,我没有注意到 Agner Fog 的 Skylake 表中的 PMOVMSKB r, v 被列为 2-3c 延迟。在 Haswell 上,VMOVMSKPS/D r32, ymm 是 2c 延迟,但 xmm 版本是 3c 延迟!这很令人惊讶。您在哪里看到 256b 版本较慢?你确定 ymm 版本在 Skylake 上不是更快吗?
  • @Jimbo:无论如何,区别最多是一个延迟周期,没有额外的微指令或吞吐量。 _mm256_movemask_epi8 仍然是您能做的最好的事情。单独使用这两个部分所做的任何事情都不会像使用一个 VPMOVMSKB r32 一样好,ymm。在上部通道上使用 128b movmsk 需要首先将其提取到寄存器的低 128b,并使用 3 周期延迟通道交叉洗牌,如 VEXTRACTF128。
  • 无论如何,请记住,测试循环条件的掩码仅对检测错误预测的延迟以及在最后一次迭代后将掩码馈送到 BSF 或 TZCNT (ffs) 的延迟敏感。带有分支预测的推测执行意味着每个条件分支指令都是一个单独的依赖链。 IE。控制依赖不是数据依赖。 JCC 标志输入的较短延迟不会影响吞吐量,只会检测到分支错误预测之前的延迟。
猜你喜欢
  • 2019-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-27
  • 2018-01-19
  • 2020-02-13
  • 2018-05-24
相关资源
最近更新 更多