【问题标题】:is there an inverse instruction to the movemask instruction in intel avx2?intel avx2中的movemask指令是否有逆指令?
【发布时间】:2016-07-29 01:44:10
【问题描述】:

movemask 指令采用 __m256i 并返回 int32,其中每个位(前 4、8 或全部 32 位,取决于输入向量元素类型)是相应向量元素的最高有效位。

我想做相反的事情:取一个 32(只有 4、8 或 32 个最低有效位有意义),并获得一个 __m256i,其中设置了每个 int8、int32 或 int64 大小的块的最高有效位到原来的位。

基本上,我想从压缩位掩码转换为可被其他 AVX2 指令(例如 maskstore、maskload、mask_gather)用作掩码的位掩码。

我无法快速找到执行此操作的指令,所以我在这里询问。 如果没有具有该功能的指令,您是否可以想到一种巧妙的技巧,只需很少的指令即可实现这一目标?

我目前的方法是使用 256 个元素的查找表。 我想在没有太多其他事情发生的循环中使用此操作,以加快速度。请注意,我对实现此操作的长多指令序列或小循环不太感兴趣。

【问题讨论】:

  • 关于潜在重复的许多好的答案,但他们主要考虑 8 位元素的情况。我在这里的回答只真正涵盖了 32 位元素的情况。 (因为较窄的元素不存在可变移位)
  • 只是好奇,你为什么不接受?

标签: x86 intrinsics avx avx2 icc


【解决方案1】:

在 AVX2 或更早版本中没有单一指令。 (AVX512可以直接使用位图形式的掩码,有将掩码扩展为向量的指令。



对于您的情况,如果您是从内存中加载位图,则将其直接加载到 ALU 策略的向量寄存器中,即使是 4 位掩码也应该可以正常工作。

如果您将位图作为计算结果,那么它将在一个整数寄存器中,您可以轻松地将其用作 LUT 索引,因此如果您的目标是 64 位元素,这是一个不错的选择。否则可能仍然会为 32 位或更小的元素使用 ALU,而不是使用巨大的 LUT 或处理多个块。


在从整数位掩码到向量掩码的廉价转换成为可能之前,我们必须等待 AVX-512 的掩码寄存器。 (使用kmovw k1, r/m16,编译器为int => __mmask16 隐式生成)。有一个 AVX512 insn 可以从掩码中设置向量(VPMOVM2D zmm1, k1_mm512_movm_epi8/16/32/64,还有其他版本用于不同的元素大小),但是您通常不需要它,因为过去使用的所有东西掩码向量现在使用掩码寄存器。也许如果您想计算满足某些比较条件的元素? (您将使用 pcmpeqd / psubd 生成和累积 0 或 -1 个元素的向量)。但是在掩码结果上标量 popcnt 会更好。

但请注意,vpmovm2d 要求掩码位于 AVX512 k0..7 掩码寄存器中。到达那里将需要额外的指令,除非它来自向量比较结果,并且移动到掩码寄存器的指令需要 Intel Skylake-X 和类似 CPU 上的端口 5 的 uop,因此这可能是一个瓶颈(特别是如果你做任何洗牌)。特别是如果它从内存中开始(加载位图)并且您只需要每个元素的高位,那么即使 256 位和 512 位 AVX512 指令可用,您也可能最好使用广播加载 + 可变移位。

也可能(对于 0/1 结果而不是 0/-1)是来自 _mm_maskz_mov_epi8(mask16, _mm_set1_epi8(1)) 等常量的零掩码负载。 https://godbolt.org/z/1sM8hY8Tj


对于 64 位元素,掩码只有 4 位,所以查找表是合理的。您可以通过使用VPMOVSXBQ ymm1, xmm2/m32. (_mm256_cvtepi8_epi64) 加载LUT 来压缩它。这为您提供了 (1pmovsx is inconvenient to use as a narrow load with intrinsics。

特别是如果您已经将位图保存在整数寄存器(而不是内存)中,vpmovsxbq LUT 在 64 位元素的内部循环中应该非常出色。或者,如果指令吞吐量或 shuffle 吞吐量是瓶颈,请使用未压缩的 LUT。这可以让您(或编译器)将掩码向量用作其他内容的内存操作数,而不需要单独的指令来加载它。


32 位元素的 LUT:可能不是最佳的,但您可以这样做

对于 32 位元素,8 位掩码为您提供 256 个可能的向量,每个向量长度为​​ 8 个。 256 * 8B = 2048 字节,即使对于压缩版本(使用 vpmovsxbd ymm, m64 加载),这也是一个相当大的缓存占用空间。

要解决此问题,您可以将 LUT 拆分为 4 位块。大约需要 3 条整数指令将一个 8 位整数拆分为两个 4 位整数 (mov/and/shr)。然后使用 128b 向量的未压缩 LUT(对于 32 位元素大小),vmovdqa 低半部分和 vinserti128 高半部分。你仍然可以压缩 LUT,但我不推荐它,因为你需要 vmovd / vpinsrd / vpmovsxbd,这是 2 次随机播放(所以你可能会成为 uop 吞吐量的瓶颈)。

或者 2x vpmovsxbd xmm, [lut + rsi*4] + vinserti128 在 Intel 上可能更糟。


ALU 替代方案:适用于 16/32/64 位元素

当整个位图适合每个元素时:广播它,使用选择器掩码进行 AND,以及 VPCMPEQ 针对同一个常量(可以在循环中多次使用 this 时保留在寄存器中)。

vpbroadcastd  ymm0,  dword [mask]            ; _mm256_set1_epi32
vpand         ymm0, ymm0,  setr_epi32(1<<0, 1<<1, 1<<2, 1<<3, ..., 1<<7)
vpcmpeqd      ymm0, ymm0,  [same constant]   ; _mm256_cmpeq_epi32
      ; ymm0 =  (mask & bit) == bit
      ; where bit = 1<<element_number

掩码可以来自带有 vmovd + vpbroadcastd 的整数寄存器,但是如果广播负载已经在内存中,则广播负载很便宜,例如从掩码数组中应用到元素数组。我们实际上只关心该 dword 的低 8 位,因为 8x 32 位元素 = 32 字节。 (例如,您从 vmovmaskps 获得)。对于 16 个 16 位元素的 16 位掩码,您需要 vpbroadcastw。要首先从 16 位整数向量中获得这样的掩码,您可以将 vpacksswb 两个向量放在一起(保留每个元素的符号位),vpermq 在通道内打包后将元素按顺序排列,然后vpmovmskb

对于 8 位元素,您需要 vpshufb vpbroadcastd 结果才能将相关位放入每个字节。见How to perform the inverse of _mm256_movemask_epi8 (VPMOVMSKB)?。但是对于 16 位和更宽的元素,元素的数量是

vpbroadcastd/q 甚至不需要任何 ALU 微指令,它直接在加载端口完成。 (bw 是加载+随机播放)。即使您的掩码被打包在一起(对于 32 位或 64 位元素,每个字节一个),使用vpbroadcastd 而不是vpbroadcastb 可能仍然更有效。 x &amp; mask == mask 检查不关心广播后每个元素的高字节中的垃圾。唯一担心的是缓存行/页面拆分。


如果您只需要符号位,则可以使用可变移位(在 Skylake 上更便宜)

变量混合和掩码加载/存储只关心掩码元素的符号位。

一旦您将 8 位掩码广播到 dword 元素,这只是 1 uop(在 Skylake 上)。

vpbroadcastd  ymm0, dword [mask]

vpsllvd       ymm0, ymm0, [vec of 24, 25, 26, 27, 28, 29, 30, 31]  ; high bit of each element = corresponding bit of the mask

;vpsrad        ymm0, ymm0, 31                          ; broadcast the sign bit of each element to the whole element
;vpsllvd + vpsrad has no advantage over vpand / vpcmpeqb, so don't use this if you need all the bits set.

vpbroadcastd 与内存负载一样便宜(在 Intel CPU 和 Ryzen 上根本没有 ALU uop)。 (更窄的广播,比如 vpbroadcastb y,mem 在 Intel 上采用 ALU shuffle uop,但可能不在 Ryzen 上。)

变量班次在 Haswell/Broadwell 上稍贵(3 微指令,有限的执行端口),但与 Skylake 上的立即计数班次一样便宜! (端口 0 或 1 上 1 uop。)在 Ryzen 上,它们也只有 2 uop(任何 256b 操作的最小值),但具有 3c 延迟和每 4c 吞吐量一个。

查看 标签wiki 以获取性能信息,尤其是Agner Fog's insn tables

对于 64 位元素,请注意算术右移仅适用于 16 位和 32 位元素大小。如果您希望 4 位 -> 64 位元素的整个元素设置为全零/全一,请使用不同的策略。

使用内在函数:

__m256i bitmap2vecmask(int m) {
    const __m256i vshift_count = _mm256_set_epi32(24, 25, 26, 27, 28, 29, 30, 31);
    __m256i bcast = _mm256_set1_epi32(m);
    __m256i shifted = _mm256_sllv_epi32(bcast, vshift_count);  // high bit of each element = corresponding bit of the mask
    return shifted;

    // use _mm256_and and _mm256_cmpeq if you need all bits set.
    //return _mm256_srai_epi32(shifted, 31);             // broadcast the sign bit to the whole element
}

在循环中,LUT 可能值得缓存占用空间,具体取决于循环中的指令组合。特别是对于 64 位元素大小,它的缓存占用空间不大,但甚至可能是 32 位。


另一种选择,而不是可变移位,是使用 BMI2 将每个位解压缩为一个字节,该掩码元素位于高位,然后 vpmovsx:

; 8bit mask bitmap in eax, constant in rdi

pdep      rax, rax, rdi   ; rdi = 0b1000000010000000... repeating
vmovq     xmm0, rax
vpmovsxbd ymm0, xmm0      ; each element = 0xffffff80 or 0

; optional
;vpsrad    ymm0, ymm0, 8   ; arithmetic shift to get -1 or 0

如果您已经在整数寄存器中设置了掩码(无论如何,您都必须分别使用 vmovq / vpbroadcastd),那么即使在可变计数移位便宜的 Skylake 上,这种方式也可能更好。

如果您的掩码从内存中开始,则其他 ALU 方法(vpbroadcastd 直接进入向量)可能更好,因为广播负载非常便宜。

请注意,pdep 是 6 个依赖于 Ryzen 的微指令(18c 延迟,18c 吞吐量),因此即使您的掩码确实以整数 regs 开头,这种方法在 Ryzen 上也很糟糕。

(未来的读者,请随意在其内部版本中进行编辑。写 asm 更容易,因为它的输入要少得多,而且 asm 助记符更容易阅读(没有愚蠢的_mm256_ 到处乱七八糟) .)

【讨论】:

  • “如果你的掩码在内存中开始,那就更糟了,因为将广播加载到向量中非常便宜。” - 你能澄清一下吗?什么更糟,什么更好?我的面具从内存开始(我在 Ryzen 上),那么我应该使用什么?
  • @SergeRogatch:那么这两个因素都支持可变移位方法。 (或者可能是压缩 LUT,因为您有 64 位元素。)
  • @PeterCordes: ALU alternative: good for 16/32/64-bit elements - 我看不出这对 16 条短裤有什么作用。我错过了什么吗?
  • @DenisYaroshevskiy:我不确定你认为会有什么问题,因为你没有提到一个。 _mm256_set1_epi16 重复 16 位掩码 16 次。 _mm256_setr_epi16(1&lt;&lt;0, 1&lt;&lt;1, ..., 1&lt;&lt;15) 的向量常数可以匹配每个元素中的一位,因为元素至少与掩码一样宽。 vpbroadcastwvpandvpcmpeqw都存在于AVX2中。
  • @DenisYaroshevskiy:我说的不是这种情况。我的答案是每 2 字节元素 1 位,您 确实 打包了位掩码。例如在vpmovmskb 之前使用vpacksswb +vpermq,以缩小保留符号位的向量元素。 32/64 位元素更容易,只需使用vmovmskps/d。如果您直接获取 _mm256_movemask_epi8 结果,它仍然是 8 位元素的字节掩码,您必须将其解压缩。 (当您了解冗余时,可能会进行一些优化)。如果其他人有同样的误解,我会考虑更新这个答案。
猜你喜欢
  • 2018-02-01
  • 2012-04-09
  • 2013-07-21
  • 2012-12-12
  • 2014-03-13
  • 1970-01-01
  • 2016-09-25
  • 1970-01-01
  • 2019-08-27
相关资源
最近更新 更多