【问题标题】:Using a variable to index a simd vector with _mm256_extract_epi32() intrinsic使用变量来索引具有 _mm256_extract_epi32() 内在函数的 simd 向量
【发布时间】:2018-07-21 09:59:31
【问题描述】:

我正在使用 AVX 内在 _mm256_extract_epi32()

我不完全确定我是否正确使用它,因为 gcc 不喜欢我的代码,而 clang 编译它并运行它没有问题。

我是根据整数变量的值提取车道,而不是使用常量。

当用clang3.8(或clang4)为avx2编译以下sn-p时,它generates code并使用vpermd指令。

#include <stdlib.h>
#include <immintrin.h>
#include <stdint.h>

uint32_t foo( int a, __m256i vec )
{
    uint32_t e = _mm256_extract_epi32( vec, a );
    return e*e;
}

现在,如果我改用 gcc,比如说 gcc 7.2,那么编译器将无法生成代码,并出现以下错误:

In file included from /opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/immintrin.h:41:0,
                 from <source>:2:
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/avxintrin.h: In function 'foo':
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/avxintrin.h:524:20: error: the last argument must be a 1-bit immediate
   return (__m128i) __builtin_ia32_vextractf128_si256 ((__v8si)__X, __N);
                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/immintrin.h:37:0,
                 from <source>:2:
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/smmintrin.h:449:11: error: selector must be an integer constant in the range 0..3
    return __builtin_ia32_vec_ext_v4si ((__v4si)__X, __N);
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

我有两个问题:

  1. 为什么clang 可以很好地使用变量,而gcc 是否想要一个 恒定?
  2. 为什么 gcc 不能下定决心?首先它需要一个 1 位立即数,然后它需要一个 0..3 范围内的整数常量,它们是不同的。

Intels Intrinsics Guide 并没有对 _mm256_extract_epi32() 的索引值指定约束,那么这里是谁,gcc 还是 clang?

【问题讨论】:

  • 注意:clang5 也会编译代码,但会往返内存以提取车道。

标签: simd intrinsics avx avx2


【解决方案1】:

显然 GCC 和 Clang 做出了不同的选择。 恕我直言,GCC 做出了正确的选择,即不对变量索引实施此操作。内在的_mm256_extract_epi32 不会转换为单个指令。使用可变索引,这种内在可能会导致代码效率低下, 如果在性能关键循环中使用它。

例如,Clang 3.8 需要 4 条指令来实现带有变量索引的_mm256_extract_epi32。 GCC 迫使程序员考虑更有效的代码,避免使用变量索引的_mm256_extract_epi32

尽管如此,有时拥有一个可移植的(gcc、clang、icc)函数很有用,它模拟_mm256_extract_epi32 带有变量 a 索引:

uint32_t mm256_extract_epi32_var_indx(const __m256i vec, const unsigned int i) {   
    __m128i indx = _mm_cvtsi32_si128(i);
    __m256i val  = _mm256_permutevar8x32_epi32(vec, _mm256_castsi128_si256(indx));
    return         _mm_cvtsi128_si32(_mm256_castsi256_si128(val));
}    

这应该在内联后编译成三个指令:两个vmovds 和一个vpermd(gcc 8.2 和-m64 -march=skylake -O3):

mm256_extract_epi32_var_indx:
  vmovd xmm1, edi
  vpermd ymm0, ymm1, ymm0
  vmovd eax, xmm0
  vzeroupper
  ret

请注意,内在函数指南描述了索引的结果为 0 >=8(无论如何这是一个不寻常的情况)。对于 Clang 3.8 和 mm256_extract_epi32_var_indx,索引以 8 为模减少。换句话说:仅使用索引的 3 个最低有效位。 请注意,Clang 5.0 的内存往返也不是很有效, 见this Godbolt link。铿锵声 7.0 无法使用变量索引编译 _mm256_extract_epi32

作为@Peter Cordes commented:使用固定索引0、1、2或3,只需一条pextrd指令即可 从 xmm 寄存器中提取整数。对于固定索引 4、5、6 或 7,需要两条指令。 不幸的是,在 256 位 ymm 寄存器上工作的 vpextrd 指令不存在。


下一个例子说明了我的答案:

一个从 SIMD 内在函数开始的天真的程序员可能会写 以下代码将元素 0、1、...、j-1 与 j&lt;8 相加,来自 vec

#include <stdlib.h>
#include <immintrin.h>
#include <stdint.h>

uint32_t foo( __m256i vec , int j)
{   
    uint32_t sum=0;
    for (int i = 0; i < j; i++){
        sum = sum + (uint32_t)_mm256_extract_epi32( vec, i );
    }
    return sum;
}

使用 Clang 3.8 编译到大约 50 个 instructions 带有分支和循环。 GCC 无法编译此代码。 显然,对这些元素求和的有效代码可能基于:

  1. 屏蔽掉元素 j、j+1、...、7 和
  2. 计算水平总和。

【讨论】:

  • 如果有更多可移植的语法用于索引矢量元素,用于调试打印功能,那就太好了。如果编译器彼此同意,_mm256_extract_epi32 似乎是放置该重载的合理位置,因为无论如何the vpextrd machine instruction 仅适用于 XMM 操作数,而不适用于 256 位 YMM。 _mm256_extract_epi32 不能是索引 >= 4 的单个指令。
  • 谢谢,但不是很关注你:如果我们忽略 >=8 索引问题,肯定是 AND 与 0x7,后跟 VPERMD 非常有效(如 clang 输出)?我尝试添加 assert(a
  • 在旧的离线内在函数指南中,复合内在函数显示了一个警告:注意:这个内在函数创建了两个或更多指令的序列,并且可能比本机指令执行得更差.考虑这个内在因素对性能的影响。 .我认为同样的警告也适用于此。当然,如果不过多使用此内在函数,则性能影响可以忽略不计。在 Skylake 上,2 vmovd-s 和 vpermd 的延迟为 7 个周期
【解决方案2】:

它说必须是 1 位立即数的 __Nnot _mm256_extract_epi32 的第二个参数,它是用作 __builtin_ia32_vextractf128_si256 的参数的一些函数(大概是第三个少量)。然后它需要一个 0..3 范围内的整数常量,用于 vpextrd,总共为您提供 3 位索引。

_mm256_extract_epi32 是一个复合内在函数,不是根据单个 builtin 函数直接定义的。

vpextrd r32, ymm, imm8 不存在,只有 xmm 版本存在,所以_mm256_extract_epi32vextracti/f128 / vpextrd 的包装。 Gcc 选择仅使其适用于编译时常量,因此它始终最多编译为 2 条指令。

如果你想要运行时变量向量索引,你需要使用不同的语法;例如存储到一个数组并加载一个标量,并希望 gcc 将其优化为 shuffle / extract。

或者定义一个具有正确元素宽度的 GNU C 原生向量类型,并使用foo[i] 像数组一样对其进行索引。

typedef int v8si __attribute__ ((vector_size (32)));
v8si tmp = foo;   // may need a cast to convert from __m256i
int element_i = tmp[i];

gcc/clang 中的__m256i 被定义为long long 元素的向量,所以如果你直接用[] 索引它,你会得到qword 元素。 (而且您的代码不会使用 MSVC 编译,它根本不会以这种方式定义 __m256i。)


我最近没有检查 asm 中的任何一个:如果您关心效率,您可能希望使用运行时变量索引手动设计随机播放,就像 @Wim 的回答表明 clang 所做的那样。

【讨论】:

    猜你喜欢
    • 2023-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多