Constexpr和SSE内置函数

14

大多数C++编译器都支持使用内在函数的SIMD(SSE/AVX)指令

_mm_cmpeq_epi32

我对此的问题是,尽管语义上没有理由不将此功能标记为constexpr,但这个函数并没有被标记为constexpr

是否有办法编写自己版本的(例如)_mm_cmpeq_epi32 以使其成为constexpr

显然,我希望函数在运行时使用正确的汇编代码,我知道我可以用慢函数重新实现任何SIMD函数,只要该函数是constexpr即可。

如果你想知道为什么我关心SIMD函数的constexpr。 非constexpr性是具有传染性的,这意味着使用那些SIMD函数的任何我的函数都不能是constexpr


返回结果:

我对此的问题是,尽管语义上没有理由不将此功能标记为constexpr,但这个函数并没有被标记为constexpr

是否有办法编写自己版本的(例如)_mm_cmpeq_epi32 以使其成为constexpr

显然,我希望函数在运行时使用正确的汇编代码,我知道我可以用慢函数重新实现任何SIMD函数,只要该函数是constexpr即可。

如果你想知道为什么我关心SIMD函数的constexpr。 非constexpr性是具有传染性的,这意味着使用那些SIMD函数的任何我的函数都不能是constexpr


2
抱歉,你运气不好。内联汇编不能在constexpr函数中使用,因此你无法编写自己的内联汇编函数。 - SergeyA
4
在标准的C++中不可能实现,但例如,GCC 定义了 __builtin_constant_p 扩展,允许使用技巧,如 #define FOO(x) (__builtin_constant_p(x) ? foo_constexpr(x) : foo_asm(x)) - 如果编译器可以将 x 评估为常量,则将使用纯C++实现,从而允许进一步的内联和编译时优化。 - dewaffled
2
编译器必须能够在编译时对 constexpr 函数进行求值。如果您的编译器不知道如何评估一些/所有SIMD内置函数,使用这些函数的方法就无法成为 constexpr。请注意,在编译时评估函数与编译函数是完全不同的; 您可能正在交叉编译到另一个平台,因此编译器甚至无法在编译后运行该函数并获取其值。因此,编译器需要特殊的仿真代码来在“纯C ++”中模拟该函数,但显然不存在。 - JimmyB
2
@JimmyB:gcc 确实知道如何在编译时评估所有 __builtin_ia32 函数;它可以像标量 int+ 运算符一样通过它们进行常量传播。这完全是 C++ 语法和声明方式的不幸问题。(唯一奇怪的是,static const __m128 foo = _mm_set1_ps(2.0f); 没有变成常量初始化程序;它在运行时从 .rodata 复制,因此很糟糕。) - Peter Cordes
1
@PeterCordes 你确定 _mm_shuffle_epi8 吗?我刚试了一下给它两个 null vectors,但它没有优化任何东西。只读常数的事情是一个众所周知的限制,目前必须在前端完成,如果编译器后来意识到它实际上是一个常数,则没有任何东西可以做。我真的希望有一天这会改变,但我不会抱太大希望。相关链接:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65197(以及55894, 80517)。 - Marc Glisse
显示剩余12条评论
2个回答

4

很遗憾,英特尔的内在函数不是以constexpr定义的。

实际上,它们完全可以被定义为constexpr。编译器可以在编译时评估它们以进行常量传播和其他优化。这也是内建函数/内在函数比单个指令的内联汇编包装更好的一个重要原因。


GCC的解决方案。(不适用于clang或MSVC)。

ICC编译它时,但在您尝试将其用作constexpr __m128i的初始化程序的一部分时会出现问题。

constexpr
__m128i pcmpeqd(__m128i a, __m128i b) {
    return (v4si)a == (v4si)b;      // fine with gcc and ICC

    //return (__m128i)__builtin_ia32_pcmpeqd128((v4si)a, (v4si)b); // bad with ICC
    //return _mm_cmpeq_epi32(a,b);  // not constexpr-compatible
}

在Godbolt编译器浏览器上查看,带有两个测试调用者(一个带有变量,一个带有
constexpr __m128i v1 {0x100000000, 0x300000002};输入)。有趣的是,ICC 不会通过pcmpeqd_mm_cmpeq_epi32进行常量传播;即使启用了优化,它也会加载两个常量并使用实际的pcmpeqd。无论是否使用constexpr,都会发生相同的事情。我认为它通常会进行优化。

gcc可以接受 constexpr __m128i vector_const { pcmpeqd(__m128i{0,0}, __m128i{-1,-1}) };


GCC(但不是clang)将__builtin_ia32函数视为constexpr兼容。GNU C x86内置函数文档没有提到这一点,但可能只是因为它是C文档,而不是C ++。

GNU C本地向量语法也是constexpr兼容的;这是第二个选项,如果您不关心MSVC,则再次可行。

GNU C将__m128i定义为两个long long元素的向量。因此,对于整数SIMD,您需要定义其他类型(或使用由gcc / clang / ICC的immintrin.h定义的类型)


唯一奇怪的是,static const __m128i foo = _mm_set1_epi32(2);并不会变成常量初始化程序;它会在运行时从.rodata复制,因此非常糟糕,使用了一个保护变量,在每次函数调用时都会检查变量是否需要静态初始化。


GCC的xmmintrin.hemmintrin.h以本机向量运算符(如*)或__builtin_ia32函数的方式定义了英特尔指令。在可能的情况下,它们更喜欢使用运算符,而不是(__m128i)__builtin_ia32_pcmpeqd128((v4si)a, (v4si)b);

gcc需要不同向量类型之间的显式强制转换。

来自gcc7.3的emmintrin.h(SSE2):

extern __inline __m128i __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_cmpeq_epi32 (__m128i __A, __m128i __B)
{
  return (__m128i) ((__v4si)__A == (__v4si)__B);
}

#ifdef __OPTIMIZE__
extern __inline __m128i __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_shuffle_epi32 (__m128i __A, const int __mask)
{
  return (__m128i)__builtin_ia32_pshufd ((__v4si)__A, __mask);
}
#else
#define _mm_shuffle_epi32(A, N) \
  ((__m128i)__builtin_ia32_pshufd ((__v4si)(__m128i)(A), (int)(N)))
#endif

有趣的是:如果在禁用优化的情况下编译,gcc的头文件会避免在某些情况下使用内联函数。我猜这会导致更好的调试符号,这样你就不会在使用GDB中的stepi时单步进入内联函数的定义(当在TUI源窗口中显示优化代码时会发生这种情况)。

3
未经优化的宏路径是因为某些指令需要一个立即常量参数,否则在 -O0 的情况下会很难获得(需要内联函数然后传递该值)。 - Marc Glisse

1
现在有一个跨平台的 c++20 解决方案。std::is_constant_evaluated 允许我们完全做到这一点。
template<typename T>
constexpr auto add(T&& l, T&& r) noexcept
{
    if (std::is_constant_evaluated())
        slow_add(std::forward<T>(l), std::forward<T>(r));
    else
        _mm_add_pd(l.value, r.value);
}

注意这里使用了普通的if语句。虽然使用if constexpr很诱人,但这将始终导致函数返回true。不要担心,分支将始终被优化掉,因为std::is_constant_evaluated的值在编译时始终是已知的(即使它返回false)。

你仍然需要一种便携式的constexpr兼容方式来实现slow_add,这可能需要使用#ifdef来以MSVC方式或GNU C本地向量方式获取元素。如果没有任何非便携式的东西,__m128i是不透明的,并且所有让您访问其元素的内部函数(包括_mm_store_si128_mm_load_si128)都没有声明为constexpr(因此出现了原始问题)。 - Peter Cordes
你是不是指使用 union { __m128i value; int32_t i32[4]; }; 或类似的方法? - Peter Cordes
相关的:如何将constexpr和向量化代码组合? 基本上有相同的答案。 - Peter Cordes
1
起初,我认为您可以使用std :: bit_cast,但我发现MSVC将__m128i和公司实现为联合,这使得在MSVC上不可能。reinterpret_cast显然在constexpr中无法工作。为了使其跨平台,您可能需要在执行_mm_load调用之前,在更高的级别上检查常量评估。 - Christopher Mauer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接