【问题标题】:Constexpr and SSE intrinsicsConsexpr 和 SSE 内在函数
【发布时间】:2019-01-23 14:13:31
【问题描述】:

大多数 C++ 编译器都支持 SIMD(SSE/AVX) 指令,如

_mm_cmpeq_epi32

我的问题是这个函数没有被标记为constexpr,尽管“语义上”没有理由让这个函数不是constexpr,因为它是一个纯函数。

有什么方法可以编写我自己的(例如)_mm_cmpeq_epi32 版本,即constexpr?

显然,我希望该函数在运行时使用正确的 asm,我知道我可以使用 constexpr 的慢速函数重新实现任何 SIMD 函数。

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

【问题讨论】:

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

标签: c++ sse simd constexpr intrinsics


【解决方案1】:

很遗憾,英特尔的内部函数没有定义为 constexpr。

他们没有理由不能这样做;编译器可以并且确实在编译时评估它们以进行常量传播和其他优化。 (这是内置函数/内在函数优于单指令的内联 asm 包装器的主要原因之一。)


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
}

See it on the Godbolt compiler explorer,有两个测试调用者(一个带有变量,一个带有
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-compatible。 documentation for GNU C x86 built-in functions 没有提及这一点,但可能只是因为它是 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.h 和emmintrin.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 源窗口的优化代码时,确实会发生这种情况。)

【讨论】:

  • 未优化的宏路径是因为某些指令需要一个立即的常量参数,否则在-O0处获取会出现问题(需要内联函数,然后传播值)。
【解决方案2】:

c++20 现在有一个跨平台的解决方案。 std::is_constant_evaluate 允许我们这样做。

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_evaluate 的值在编译时总是已知的(即使它返回 false)。

【讨论】:

  • 您仍然需要一种可移植的 constexpr 兼容方式来实现 slow_add,这可能需要 #ifdef 以 MSVC 方式或 GNU C 本机向量方式获取元素。没有任何不可移植的东西,__m128i 是不透明的,并且所有可以让您了解其元素的内在函数(包括 _mm_store_si128 和 _mm_load_si128)都没有声明为 constexpr(因此是原始问题)。
  • 或者您的意思是使用union { __m128i value; int32_t i32[4]; }; 或类似名称?
  • 相关:How to combine constexpr and vectorized code? 的答案基本相同。
  • 起初,我以为你可以使用 std::bit_cast,但我看到 MSVC 将 __m128i 和 company 实现为联合,这在 MSVC 上是不可能的。 reinterpret_cast 显然在 constexpr 中不起作用。为了实现这个跨平台,您可能需要在执行 _mm_load 调用之前检查更高级别的持续评估。
猜你喜欢
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 2011-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-20
  • 2021-12-11
相关资源
最近更新 更多