我正在使用 _mm_extract_epi8 (__m128i a, const int imm8)
函数,它有 const int
参数。当我编译此 c++ 代码时,收到以下错误消息:
Error C2057 expected constant expression
__m128i a;
for (int i=0; i<16; i++)
{
_mm_extract_epi8(a, i); // compilation error
}
如何在循环中使用这个函数?
首先,即使可能,您也不会希望在循环中使用它,并且您不会希望使用 16x pextrb
完全展开循环。该指令在 Intel 和 AMD CPU 上花费 2 微指令,并且会在随机端口(以及用于 vec->int 数据传输的端口 0)上出现瓶颈。
_mm_extract_epi8
内在函数需要编译时常量索引,因为 the pextrb r32/m8, xmm, imm8
instruction仅适用于作为立即数的索引(嵌入到指令的机器代码中)。
如果您想放弃 SIMD 并在 vector 元素上编写标量循环,您应该存储/重新加载这么多元素。所以你应该用 C++ 这样写:
alignas(16) int8_t bytes[16]; // or uint8_t
_mm_store_si128((__m128i*)bytes, vec);
for(int i=0 ; i<16 ; i++) {
foo(bytes[i]);
}
一次存储的成本(以及存储转发延迟)分摊到 16 次重新加载中,每次重新加载仅花费 1 movsx eax, byte ptr [rsp+16]
或任何其他内容。 (在 Intel 和 Ryzen 上为 1 uop)。或者在重新加载时使用 uint8_t
将 movzx
零扩展到 32 位。现代 CPU 每个时钟可以运行 2 个加载微指令, vector 存储 -> 标量重新加载存储转发是高效的(~6 或 7 个周期延迟)。
对于 64 位元素,movq
+ pextrq
几乎肯定是您的最佳选择。存储 + 重新加载的前端成本相当,延迟比提取更差。
对于 32 位元素,它更接近收支平衡,具体取决于您的循环。如果循环体很小,展开的 ALU 提取可能会很好。或者您可以存储/重新加载但确实使用 _mm_cvtsi128_si32
(movd
) 执行第一个元素以在第一个元素上实现低延迟,以便 CPU 可以在存储时处理它-发生高元素的转发延迟。
对于 16 位或 8 位元素,如果您需要遍历所有 8 位或 16 位元素,几乎可以肯定存储/重新加载更好。
如果您的循环对每个元素进行非内联函数调用,Windows x64 调用约定有一些调用保留的 XMM 寄存器,但 x86-64 System V 没有。因此,如果您的 XMM reg 需要围绕函数调用进行溢出/重新加载,多最好只进行标量加载,因为编译器无论如何都会将其保存在内存中。 (希望它可以优化它的第二个拷贝,或者您可以声明一个 union 。)
见
print a __m128i variable用于所有元素大小的工作存储 + 标量循环。
如果您真的想要一个水平总和,或者最小值或最大值,您可以在 O(log n) 步中随机播放,而不是 n 次标量循环迭代。 Fastest way to do horizontal float vector sum on x86 (还提到了 32 位整数)。
对于字节元素求和,SSE2 有一个特例 _mm_sad_epu8(vec, _mm_setzero_si128())
。 Sum reduction of unsigned bytes without overflow, using SSE2 on Intel .
您还可以使用它来处理有符号字节,方法是将范围转换为无符号字节,然后从总和中减去 16*0x80
。 https://github.com/pcordes/vectorclass/commit/630ca802bb1abefd096907f8457d090c28c8327b