如何使用 _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_tmovzx 零扩展为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的操作就不需要 元素的循环(示例可能包含一些自我宣传):

水平最小值、最大值、总和、绝对值总和、均方根、平均值、bitand、bitor。 Prefix sum。 计算出现频率最高的元素(the modus)。 Variabele bit shift. 创建mask based on byte values.。 计算indices of the nonzero elements。 等。等等。

在这些情况下,有效的矢量化解决方案是可能的。

如果您无法避免性能关键循环中的元素循环: 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, zmm1k1 = 1&lt;&lt;idx 更好,因为在大多数 CPU 上 vpermd 比压缩更快。

以上是关于如何使用 _mm_extract_epi8 函数? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

如何使用非立即输入进行类似于 _mm_extract_epi8 的操作?

如何在 SSE 中使用 imm8?

如何在 Neon 内在函数中使用 if 条件进行并行操作?

如何从 __m64 值的 lsb 创建一个 8 位掩码?

如何强制我的本地 Azure 函数服务器使用 HTTP 2.0 而不是 1.1?

如何使用 ctypes 调用具有双下划线函数名的 DLL 函数?