【问题标题】:How can I set __m128i without using of any SSE instruction?如何在不使用任何 SSE 指令的情况下设置 __m128i?
【发布时间】:2016-05-18 00:29:51
【问题描述】:

我有许多函数使用相同的常量 __m128i 值。 例如:

const __m128i K8 = _mm_setr_epi8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
const __m128i K16 = _mm_setr_epi16(1, 2, 3, 4, 5, 6, 7, 8);
const __m128i K32 = _mm_setr_epi32(1, 2, 3, 4);

所以我想将所有这些常量存储在一个地方。 但是有一个问题:我在运行时检查现有的 CPU 扩展。 如果 CPU 不支持例如 SSE(或 AVX),那么在常量初始化期间将导致程序崩溃。

那么是否可以在不使用 SSE 的情况下初始化这些常量?

【问题讨论】:

  • 我不认为这是个问题。即使它们是全局常量,编译器也会将它们转换为在运行时从“数据”部分加载的浮点数据包(常量值)。假设您仅在已验证支持 SSE 指令的代码路径中使用那些常量,那么您将永远不会在不支持它们的平台上执行任何 SSE 指令。检查汇编代码输出以确定。

标签: c++ constants sse simd sse2


【解决方案1】:

你通常不需要这个。编译器非常擅长为使用相同常量的多个函数使用相同的存储。就像将相同字符串文字的多个实例合并为一个字符串常量一样,不同函数中相同_mm_set* 的多个实例都将从同一个向量常量加载(或generate on the fly 用于_mm_setzero_si128()_mm_set1_epi8(-1))。

使用 Godbolt 的二进制输出(反汇编)模式可以让您查看不同的函数是否从同一个内存块加载。查看它添加的注释,它将 RIP 相对地址解析为绝对地址。

  • gcc:all identical constants share the same storage,无论它们来自自动矢量化还是_mm_set。 32B 常量不能与 16B 常量重叠,即使 16B 常量是 32B 的子集。

  • 叮当声:identical constants share storage。 16B 和 32B 常数不重叠,即使其中一个是另一个的子集。一些使用重复常量的函数使用 AVX2 vpbroadcastd 广播负载(在 Intel SnB 系列 CPU 上甚至不需要 ALU uop)。出于某种原因,它选择基于操作的元素大小而不是常数的重复性来执行此操作。请注意,clang 的 asm 输出每次使用都会重复该常量,但最终的二进制文件不会。

  • MSVC:identical constants share storage。与 gcc 所做的几乎相同。 (完整的 asm 输出很难通过;使用搜索。我只能通过让 main 找到 .exe 的路径来获得 asm,然后计算出使用 cl.exe -O2 /FAs 生成的 asm 输出的路径,并运行system("type .../foo.asm"))。

编译器擅长于此,因为这不是一个新问题。从最早的编译器开始,它就与字符串一起存在。

我没有检查这是否适用于源文件(例如,对于在多个编译单元中使用的内联向量函数)。如果您确实仍然需要静态/全局向量常量,请参见下文:


似乎没有简单的可移植方式来静态初始化静态/全局__m128。 C 编译器甚至不会接受 _mm_set* 作为初始化程序,因为它像函数一样工作。他们没有利用这样一个事实,即他们实际上可以通过它看到编译时间常数 16B

const __m128i K32 = _mm_setr_epi32(1, 2, 3, 4);   // Illegal in C
// C++: generates a constructor that copies from .rodata to the BSS

尽管构造函数只需要 SSE1 或 SSE2,但无论如何您都不想要这个。这太糟糕了。 不要这样做。你最终会支付两次常量的内存成本。


Fabio 的 union 答案看起来是静态初始化向量常量的最佳可移植方式,但这意味着您必须访问 __m128i 联合成员。即使它们被分散的函数使用,它也可能有助于将彼此靠近的相关常量分组(希望在同一个缓存行中)。也有一些不可移植的方法来完成(例如,将相关常量放在它们自己的 ELF 部分中,使用GNU C __attribute__ ((section ("constants_for_task_A"))))。希望这可以将它们组合到.rodata 部分(成为.text 部分的一部分)。

【讨论】:

    【解决方案2】:

    您可以使用联合。

    union M128 {
       char[16] i8;
       __m128i i128;
    };
    
    const M128 k8 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
    

    如果 M128 联合是在您使用循环的本地定义的,那么这应该没有性能开销(它将在循环开始时加载到内存中一次)。因为它包含一个 __m128i 类型的变量,所以 M128 继承了正确的对齐方式。

    void foo()
    {
       M128 k8 = ...;
       // use k8.i128 in your for loop
    }
    

    如果是在别处定义的,那么在开始循环之前需要拷贝到本地寄存器中,否则编译器可能无法优化。

    void foo()
    {
        __m128i tmp = k8.i128;
        // for loop here
    }
    

    这会将 k8 加载到 cpu 寄存器中,并在循环期间将其保留在那里,只要有足够的空闲寄存器来执行循环体。

    根据您使用的编译器,这些联合可能已经定义(VS 已定义),但编译器提供的定义可能不可移植。

    【讨论】:

    • 正如我在回答中提到的,k8 is 保证是对齐的。联合选择了大小和对齐方式,以便其所有成员都能正常工作。
    【解决方案3】:

    可以在不使用 SSE 指令的情况下初始化 __m128i 向量,但这取决于编译器如何定义 __m128i。

    对于 Microsoft Visual Studio,您可以定义下一个宏(它将 __m128i 定义为 char[16]):

    template <class T> inline char GetChar(T value, size_t index)
    {
        return ((char*)&value)[index];
    }
    
    #define AS_CHAR(a) char(a)
    
    #define AS_2CHARS(a) \
        GetChar(int16_t(a), 0), GetChar(int16_t(a), 1)
    
    #define AS_4CHARS(a) \
        GetChar(int32_t(a), 0), GetChar(int32_t(a), 1), \
        GetChar(int32_t(a), 2), GetChar(int32_t(a), 3)
    
    #define _MM_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
        {AS_CHAR(a0), AS_CHAR(a1), AS_CHAR(a2), AS_CHAR(a3), \
         AS_CHAR(a4), AS_CHAR(a5), AS_CHAR(a6), AS_CHAR(a7), \
         AS_CHAR(a8), AS_CHAR(a9), AS_CHAR(aa), AS_CHAR(ab), \
         AS_CHAR(ac), AS_CHAR(ad), AS_CHAR(ae), AS_CHAR(af)}
    
    #define _MM_SETR_EPI16(a0, a1, a2, a3, a4, a5, a6, a7) \
        {AS_2CHARS(a0), AS_2CHARS(a1), AS_2CHARS(a2), AS_2CHARS(a3), \
         AS_2CHARS(a4), AS_2CHARS(a5), AS_2CHARS(a6), AS_2CHARS(a7)}
    
    #define _MM_SETR_EPI32(a0, a1, a2, a3) \
        {AS_4CHARS(a0), AS_4CHARS(a1), AS_4CHARS(a2), AS_4CHARS(a3)}       
    

    对于 GCC,它将是(它将 __m128i 定义为 long long[2]):

    #define CHAR_AS_LONGLONG(a) (((long long)a) & 0xFF)
    
    #define SHORT_AS_LONGLONG(a) (((long long)a) & 0xFFFF)
    
    #define INT_AS_LONGLONG(a) (((long long)a) & 0xFFFFFFFF)
    
    #define LL_SETR_EPI8(a, b, c, d, e, f, g, h) \
        CHAR_AS_LONGLONG(a) | (CHAR_AS_LONGLONG(b) << 8) | \
        (CHAR_AS_LONGLONG(c) << 16) | (CHAR_AS_LONGLONG(d) << 24) | \
        (CHAR_AS_LONGLONG(e) << 32) | (CHAR_AS_LONGLONG(f) << 40) | \
        (CHAR_AS_LONGLONG(g) << 48) | (CHAR_AS_LONGLONG(h) << 56)
    
    #define LL_SETR_EPI16(a, b, c, d) \
        SHORT_AS_LONGLONG(a) | (SHORT_AS_LONGLONG(b) << 16) | \
        (SHORT_AS_LONGLONG(c) << 32) | (SHORT_AS_LONGLONG(d) << 48)
    
    #define LL_SETR_EPI32(a, b) \
        INT_AS_LONGLONG(a) | (INT_AS_LONGLONG(b) << 32)        
    
    #define _MM_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
        {LL_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7), LL_SETR_EPI8(a8, a9, aa, ab, ac, ad, ae, af)}
    
    #define _MM_SETR_EPI16(a0, a1, a2, a3, a4, a5, a6, a7) \
        {LL_SETR_EPI16(a0, a1, a2, a3), LL_SETR_EPI16(a4, a5, a6, a7)}
    
    #define _MM_SETR_EPI32(a0, a1, a2, a3) \
        {LL_SETR_EPI32(a0, a1), LL_SETR_EPI32(a2, a3)}        
    

    所以在你的代码中初始化 __m128i 常量将如下所示:

    const __m128i K8 = _MM_SETR_EPI8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
    const __m128i K16 = _MM_SETR_EPI16(1, 2, 3, 4, 5, 6, 7, 8);
    const __m128i K32 = _MM_SETR_EPI32(1, 2, 3, 4);
    

    【讨论】:

    • 对不起,我不明白所有这些代码的目的是什么。它如何回答这个问题?他已经知道如何初始化常量。为什么你的代码是一种更好的方法?
    • 这是问题的直接答案。作者询问了没有使用 SSE 指令的 __m128i 的初始化。我举了初始化的例子。
    • 啊,我明白了。这就是我所缺少的。我在答案中添加了说明。
    • 我检查了汇编输出。它不包含 SSE 指令。谢谢!
    【解决方案4】:

    我建议将初始化数据全局定义为标量数据,然后在本地将其加载到const __m128i

    static const uint8_t gK8[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
    
    static inline foo()
    {
        const __m128i K8 = _mm_loadu_si128((__m128i *)gK8);
    
        // ...
    }
    

    【讨论】:

    • @ErmIg:嗯,这是一条指令,通常在函数入口处(为什么要多次加载常量?),所以除非它是一个非常小的函数,或者很少调用加载常量会导致缓存未命中,我认为成本并不高。当然是 YMMV。
    • @Paul_R:如果常量在带有循环的函数的开头初始化,则为 true。但是如果我们想在内联函数中使用常量并且在循环中调用这个内联函数,我会看到性能开销。
    • @ErmIg:您是在谈论这样一个事实,即使用非 AVX 代码,未对齐的加载不能作为内存操作数折叠到 ALU 操作中?我认为如果 gcc 和 clang 位于循环中使用的内联函数中,它将将该负载提升出循环。无论如何,最好有对齐的数据。我不认为这是必要的,因为具有常量输入的set1 内在函数已经产生了位于.rodata 部分的编译时常量。您不会看到构造函数运行 SSE 指令以在运行时生成和存储常量。
    • @Paul-R:是的,在大多数情况下没有开销。
    • @ErmIg 和 Paul:我对此进行了调查:set 内在函数作为全局/静态变量的初始值设定项实际上确实 产生了构造函数。事实证明,编译器真的很擅长在多个函数中合并同一个常量的定义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-10
    • 2012-05-05
    • 2011-12-31
    • 1970-01-01
    • 2012-04-23
    • 2021-11-05
    • 1970-01-01
    相关资源
    最近更新 更多