【问题标题】:fC - How can I define SIMD variable(s) outside of a function?fC - 如何在函数之外定义 SIMD 变量?
【发布时间】:2021-10-28 14:07:13
【问题描述】:
    const __m128i ___n = _mm_set_epi32( 0x80808080, 0x80808080, 0x80808080, 0x80808080 );
    const __m128i w___ = _mm_set_epi32( 0x80808080, 0x80808080, 0x80808080, 0x0f0e0d0c );
    const __m128i z___ = _mm_set_epi32( 0x80808080, 0x80808080, 0x80808080, 0x0b0a0908 );
    const __m128i zw__ = _mm_set_epi32( 0x80808080, 0x80808080, 0x0f0e0d0c, 0x0b0a0908 );
    const __m128i y___ = _mm_set_epi32( 0x80808080, 0x80808080, 0x80808080, 0x07060504 );
    const __m128i yw__ = _mm_set_epi32( 0x80808080, 0x80808080, 0x0f0e0d0c, 0x07060504 );
    const __m128i yz__ = _mm_set_epi32( 0x80808080, 0x80808080, 0x0b0a0908, 0x07060504 );
    const __m128i yzw_ = _mm_set_epi32( 0x80808080, 0x0f0e0d0c, 0x0b0a0908, 0x07060504 );
    const __m128i x___ = _mm_set_epi32( 0x80808080, 0x80808080, 0x80808080, 0x03020100 );
    const __m128i xw__ = _mm_set_epi32( 0x80808080, 0x80808080, 0x0f0e0d0c, 0x03020100 );
    const __m128i xz__ = _mm_set_epi32( 0x80808080, 0x80808080, 0x0b0a0908, 0x03020100 );
    const __m128i xzw_ = _mm_set_epi32( 0x80808080, 0x0f0e0d0c, 0x0b0a0908, 0x03020100 );
    const __m128i xy__ = _mm_set_epi32( 0x80808080, 0x80808080, 0x07060504, 0x03020100 );
    const __m128i xyw_ = _mm_set_epi32( 0x80808080, 0x0f0e0d0c, 0x07060504, 0x03020100 );
    const __m128i xyz_ = _mm_set_epi32( 0x80808080, 0x0b0a0908, 0x07060504, 0x03020100 );
    const __m128i xyzw = _mm_set_epi32( 0x0f0e0d0c, 0x0b0a0908, 0x07060504, 0x03020100 );
    const __m128i LUT[16] = { ___n, x___, y___, xy__, z___, xz__, yz__, xyz_, w___, xw__, yw__, xyw_, zw__, xzw_, yzw_, xyzw };

对于比较和左打包例程的 SSE/SSSE3 版本,我使用了与上述类似的查找表。一组比较每秒发生多次(60)次,我希望它在内存中而不是每次都设置,并且范围仅限于一个 .c 文件,但尝试在有或没有静态收益的函数中定义它错误:initializer element is not constant.对于每个“集合”。为什么会发生这种情况,我该如何正确地做到这一点?

【问题讨论】:

  • _mm_set_epi32的定义?
  • @alinsoar 编译器内部通常。
  • 在函数之外定义变量并不能确保它们会在内存中。此外,每秒 60 次在当今典型机器的性能中并不多。

标签: c simd sse intrinsics


【解决方案1】:

编译器报告初始化器元素不是常量,因为_mm_set_epi32 是一个函数调用,不满足初始化器的“常量”要求。此外,您定义的各种变量 ___n 等不符合用于初始化 LUT 的常量。

你可以定义你的数组:

const __v4su LUT[16] =
{
    { 0x80808080, 0x80808080, 0x80808080, 0x80808080 },
    { 0x03020100, 0x80808080, 0x80808080, 0x80808080 },
    { 0x07060504, 0x80808080, 0x80808080, 0x80808080 },
    { 0x03020100, 0x07060504, 0x80808080, 0x80808080 },
    { 0x0b0a0908, 0x80808080, 0x80808080, 0x80808080 },
    { 0x03020100, 0x0b0a0908, 0x80808080, 0x80808080 },
    { 0x07060504, 0x0b0a0908, 0x80808080, 0x80808080 },
    { 0x03020100, 0x07060504, 0x0b0a0908, 0x80808080 },
    { 0x0f0e0d0c, 0x80808080, 0x80808080, 0x80808080 },
    { 0x03020100, 0x0f0e0d0c, 0x80808080, 0x80808080 },
    { 0x07060504, 0x0f0e0d0c, 0x80808080, 0x80808080 },
    { 0x03020100, 0x07060504, 0x0f0e0d0c, 0x80808080 },
    { 0x0b0a0908, 0x0f0e0d0c, 0x80808080, 0x80808080 },
    { 0x03020100, 0x0b0a0908, 0x0f0e0d0c, 0x80808080 },
    { 0x07060504, 0x0b0a0908, 0x0f0e0d0c, 0x80808080 },
    { 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c },
};

由于类型从__m128i 更改为__v4su,您可能需要强制转换才能使用它。 (向量运算就是为此而设计的。)

但是,在任何函数之外定义它和/或使用静态存储持续时间并不能确保它会在内存中。

【讨论】:

  • 您可能需要颠倒元素的顺序,因为_mm_set_epi32 采用大端顺序的参数。
  • 只是好奇,_mm_set_epi32 的定义(语义上)是什么?
  • @alinsoar:它需要四个 int 值,以相反的顺序将它们生成一个 __v4si 复合文字,并将其转换为 __m128i,这是一个 16 字节的两个向量long long(64 位)元素:static __inline__ __m128i __DEFAULT_FN_ATTRS _mm_set_epi32(int __i3, int __i2, int __i1, int __i0) { return __extension__ (__m128i)(__v4si){ __i0, __i1, __i2, __i3}; }
  • 感谢埃里克!出于好奇,在任何函数之外定义的静态变量位于何处? .c 中的每个函数都可以使用它,那为什么不在内存中呢?
  • @Timothys:静态数据通常在程序的数据部分。它位于进程的虚拟内存中。但该内存是虚拟的:它由操作系统管理。当程序实际使用它时,它只需要在物理内存中。除此之外,系统可能会从物理内存中删除它(如果程序再次需要它,它可以稍后恢复它)。这通常适用于进程的所有内存:静态数据部分、堆栈、分配的内存,甚至是代码。
【解决方案2】:

_mm_set_epi32 with compile-time-constant args 被优化为向量常量,通常从内存中加载。你不需要帮助编译器,事实上,如果你尝试是因为编译器非常不擅长它。如果您确实需要/想要帮助编译器进行常量布局,请使用某种类型的数组,例如 alignas(16) static const int32_t LUT[] = {...}; 并使用 _mm_load_si128( (__m128i*)&LUT[i*4] ) 或类似的东西。

像全零或全1位这样的简单向量甚至可以通过pxor xmm0,xmm0pcmpeqd xmm1, xmm1在寄存器中实现,甚至比加载更有效,因此确保编译器在优化函数时可以看到常量值使用 _mm_set* 内部函数定义常量是一个很好的理由。

_mm_set_epi32 视为字符串文字:编译器会计算出将其放置在何处,如果多个函数需要相同的向量常量,甚至会进行重复合并。 (与字符串文字不同,它是一个值,而不是衰减为指向存储的指针的东西,因此只有部分类比有效。)


_mm_set* 唯一的好地方是内部函数;当前的编译器在有效处理__m128i 类型的全局/static 变量方面很糟糕,未能将其作为静态初始化程序处理,因此他们实际上在.rodata 部分中放置了一个匿名向量常量,并具有运行时构造函数/初始化程序函数将其复制到 .bss 中的空间,用于静态存储中的命名变量。

(在 C 中,这只能发生在函数内部的 static __m128i 上,这使得它需要一个保护变量。在 C++ 中,允许非常量的全局初始化器,例如 int foo = bar(123); 在全局范围内,即使 @987654338 @ 不是 constexpr。在 C 中,您会遇到遇到的错误。)

例如:

#include <immintrin.h>

__m128i foo() {
    return _mm_setr_epi32(1,2,3,4);
}

使用 GCC11.2 -O3 (on Godbolt) 编译到这个 asm。 (clang 和 ICC,我认为 MSVC,对于以下所有代码块都是相似的。)

    # in .text
foo():
        movdqa  xmm0, XMMWORD PTR .LC0[rip]
        ret

    # in .rodata
.LC0:
        .quad   8589934593     # 0x200000001
        .quad   17179869187    # 0x400000003
__m128i bar(__m128i v) {
    return _mm_add_epi32(v, _mm_setr_epi32(1,2,3,4));
}
bar(long long __vector(2)):
        paddd   xmm0, XMMWORD PTR .LC1[rip]
        ret

        .set    .LC1,.LC0     # make LC1 a synonym for LC0
    # GCC noticed and merged at compile time, not leaving it for the linker.
__m128i retzero() {
    //return _mm_setzero_si128();
    return _mm_setr_epi32(0,0,0,0);  // optimizes the same
}

        pxor    xmm0, xmm0
        ret

但这是您从 C++ 编译器中获得的全局向量

__m128i globvec = _mm_setr_epi32(1,2,3,4);
_GLOBAL__sub_I_foo():                          # static initializer code
        movdqa  xmm0, XMMWORD PTR .LC0[rip]        # copy from anonymous .rodata
        movaps  XMMWORD PTR globvec[rip], xmm0     # to named .bss space
        ret
# in .bss
globvec:
        .zero   16

这就是 C 错误消息“拯救”你的地方; C 不允许非常量静态初始值设定项(函数除外)。

对于函数内部的static __m128i,情况会更糟:您将获得一个保护变量以确保非常量初始化程序仅在第一次调用时运行。


实际使用globvec 的代码基本上没问题,它会将其用作内存源操作数或正常加载它,但任何优化例如基于某些元素具有通过某些操作进行恒定传播的已知值是不可能的。您还使用了两倍的空间,尽管初始化程序数据在启动期间仅被触及一次,因此不会影响缓存占用空间。

【讨论】:

【解决方案3】:

或者:

constexpr _m128 _val = {0.5f,0.5f,0.5f,0.5f};

【讨论】:

  • 这适用于 FP 向量,但不适用于 __m128i 向量,就像这个问题所问的那样。在 GNU C 中,__m128itypedef long long __m128i __attribute__((vector_size(16), may_alias)),因此您必须手动组合像 {0x100000001, 0x100000001} 这样的 32 位初始化器来模拟 set1_epi32( 1 )
  • 更糟糕的是,__m128i 是 Visual C++ 中一堆东西的联合体,它编译 与 MSVC,但不是正确的值。而是等效于_mm_set_epi32(0,0,0,1)godbolt.org/z/e1h66c4o6(如 cmets 在 Is generally faster do _mm_set1_ps once and reuse it or set it on the fly? 中讨论的那样)
猜你喜欢
  • 2019-02-07
  • 1970-01-01
  • 2019-01-07
  • 2023-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多