如何使用 _mm_extract_epi8 函数? [复制]
Posted
技术标签:
【中文标题】如何使用 _mm_extract_epi8 函数? [复制]【英文标题】:How to use _mm_extract_epi8 function? [duplicate] 【发布时间】:2019-02-02 12:10:07 【问题描述】:我正在使用_mm_extract_epi8 (__m128i a, const int imm8)
函数,它有const int
参数。当我编译此 c++ 代码时,收到以下错误消息:
错误 C2057 预期常量表达式
__m128i a;
for (int i=0; i<16; i++)
_mm_extract_epi8(a, i); // compilation error
如何循环使用这个函数?
【问题讨论】:
你不能,我不是一个常数。除非您展开循环并编写_mm_extract_epi8(a,0)
、_mm_extract_epi8(a,1)
等。
不可移植,你可以使用a.m128i_u8[i]
。对于其他编译器,您可以使用 __m128i 和 char[16] 的联合。但如果您需要像这样迭代矢量元素,这不是一个好兆头。
可移植的方式是将memcpy
的内容放到char[16]
数组中并访问元素。在C
中,union
也可以。你到底想做什么?
@chtz: 不是 memcpy,_mm_storeu_si128
:P 或 alignas(16) char bytes[16]
,所以你可以使用 _mm_store_si128
。如果你想一次循环一个向量的字节,存储/重新加载比展开的 16x pextrb
更有效。
@chtz:您不希望它跨越缓存行边界。在通常不会破坏存储转发的现代 CPU 上,但在实际提交 L1d 时仍会消耗额外资源。回复:定义:我对英特尔的内在函数也没有印象。太多关于提供什么的糟糕决定(例如,没有内存源 pmovzx*
内在的,编译器很难优化掉 movd 或 movq 负载以创建 __m128i
),以及笨拙的命名。不过,最后使用 AVX512,整数加载/存储需要 void*
。
【参考方案1】:
首先,即使可能,您也不希望在循环中使用它,并且您不希望使用 16x pextrb
完全展开循环。该指令在 Intel 和 AMD CPU 上花费 2 微秒,并且会在 shuffle 端口(以及用于 vec->int 数据传输的端口 0)上成为瓶颈。
_mm_extract_epi8
内在函数需要一个编译时常量索引,因为 the pextrb r32/m8, xmm, imm8
instruction 仅在索引作为立即数时可用(嵌入到指令的机器代码中)。
如果你想放弃 SIMD 并在向量元素上编写一个标量循环,对于这么多元素你应该存储/重新加载。所以你应该在 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]
或其他任何值。 (英特尔和锐龙 1 uop)。或者在重新加载时使用uint8_t
将movzx
零扩展为32 位。现代 CPU 每个时钟可以运行 2 个加载微指令,并且向量存储 -> 标量重新加载存储转发是高效的(约 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 需要在函数调用周围溢出/重新加载,那么只进行标量加载会更好,因为编译器无论如何都会将其保存在内存中。 (希望它可以优化掉它的第二个副本,或者你可以声明一个联合。)
看 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
【讨论】:
【参考方案2】:内在 _mm_extract_epi8()
不能与变量索引一起使用,
正如在 cmets 中已经指出的那样。
您可以改用下面的解决方案,
但我只会在非性能关键循环中使用此解决方案,
例如,将结果打印到文件或屏幕。
实际上,在实践中几乎不需要循环
xmm
的字节元素。比如下面对epi8
的操作就不需要
元素的循环(示例可能包含一些自我宣传):
在这些情况下,有效的矢量化解决方案是可能的。
如果您无法避免性能关键循环中的元素循环: Peter Cordes'solution 应该 比下面的快, 至少如果您必须提取许多(2 个或更多)元素。
#include <stdio.h>
#include <stdint.h>
#include <immintrin.h>
/* gcc -m64 -O3 -march=nehalem extr_byte.c */
uint8_t mm_extract_epi8_var_indx(__m128i vec, int i )
__m128i indx = _mm_cvtsi32_si128(i);
__m128i val = _mm_shuffle_epi8(vec, indx);
return (uint8_t)_mm_cvtsi128_si32(val);
int main()
int i;
__m128i x = _mm_set_epi8(36,35,34,33, 32,31,30, 29,28,27,26, 25,24,23,22,21);
uint8_t t;
for (i = 0; i < 16; i++)
printf("x_%i = ", i);
t = mm_extract_epi8_var_indx(x, i);
printf("%i \n", t);
return 0;
结果:
$ ./a.out
x_0 = 21
x_1 = 22
x_2 = 23
x_3 = 24
x_4 = 25
x_5 = 26
x_6 = 27
x_7 = 28
x_8 = 29
x_9 = 30
x_10 = 31
x_11 = 32
x_12 = 33
x_13 = 34
x_14 = 35
x_15 = 36
【讨论】:
如果我们在 SSE2 中有双字变量洗牌,我们可以使用它(以及右移来处理索引的字内字节部分),但我们没有得到 @987654332 @ 直到 AVX2。但是然后确定对 YMM 或 ZMM 中的字节的可变访问可能。我认为这比vpcompressd zmm0k1, zmm1
和 k1 = 1<<idx
更好,因为在大多数 CPU 上 vpermd
比压缩更快。以上是关于如何使用 _mm_extract_epi8 函数? [复制]的主要内容,如果未能解决你的问题,请参考以下文章
如何使用非立即输入进行类似于 _mm_extract_epi8 的操作?