【问题标题】:are static / static local SSE / AVX variables blocking a xmm / ymm register?静态/静态本地 SSE/AVX 变量是否阻塞了 xmm/ymm 寄存器?
【发布时间】:2023-12-21 16:18:01
【问题描述】:

使用 SSE 内在函数时,通常需要零向量。避免在函数被调用时(每次有效地调用一些异或向量指令)在函数内部创建零变量的一种方法是使用静态局部变量,如

static inline __m128i negate(__m128i a)
{
   static __m128i zero = __mm_setzero_si128();
   return _mm_sub_epi16(zero, a);
}

似乎该变量仅在第一次调用该函数时才被初始化。 (我通过调用一个真正的函数而不是 _mm_setzero_si128() 内在函数来检查这一点。顺便说一句,这似乎只能在 C++ 中实现,而不是在 C 语言中。)

(1) 然而,一旦这个初始化发生:这是否会为程序的其余部分阻塞一个 xmm 寄存器?

(2) 更糟糕的是:如果在多个函数中使用这样的静态局部变量,会不会阻塞多个xmm寄存器?

(3) 反过来:如果它阻塞了一个xmm寄存器,当函数被调用时,零变量是否总是从内存中重新加载?那么静态局部变量将毫无意义,因为使用 _mm_setzero_si128() 会更快。

作为替代方案,我正在考虑将零放入将在程序启动时初始化的全局静态变量:

static __m128i zero = _mm_setzero_si128();

(4) 程序运行时全局变量是否会保留在 xmm 寄存器中?

非常感谢您的帮助!

(因为这也适用于 AVX 内在函数,所以我还添加了 AVX 标签。)

【问题讨论】:

  • 1 和 2,用 16 个静态变量创建 16 个函数,看看是否编译和运行。 4、创建16个全局变量,看是否编译运行。 3 受编译器优化的影响。 (顺便说一句,您正在进行微优化,因为在当前处理器上将寄存器归零基本上是免费的)
  • 如果你广播一个非零值,你的问题会更有趣,例如_mm_set1_epi32(-1).
  • 原来-1 也很特别。所以广播除了0-1 之外的任何东西都会很有趣。例如_mm_set1_epi32(1).
  • 感谢@Zboson,提供两个有用的链接。关于您的最后一条评论:为什么 -1 也很特别?另外,我查看了广播操作(至少对于 -mssse3 而言),它们似乎非常昂贵(编译成两条指令,其中一条是向量随机播放)。

标签: c++ sse avx


【解决方案1】:

回答真正应该在这里提出的问题:您根本不应该担心这个。在大多数情况下,通过xor 将寄存器归零实际上并不花费任何成本。现代 x86 处理器识别这个习语并直接在寄存器重命名中处理归零;根本不需要发出微指令。唯一可以减慢您速度的情况是您受前端约束,但这种情况很少见。

虽然在其他情况下这些问题的变化可能值得思考(Mystical 的评论提供了一些关于如何自己回答这些问题的良好线索),但您真的应该使用 setzero 并收工。

【讨论】:

  • 我不知道(或者我忘记了)这是通过寄存器重命名免费处理的。这太酷了。我读了一下randomascii.wordpress.com/2012/12/29/…显然SB有一个物理零寄存器,它只是将寄存器重命名为零寄存器,这就是它不需要微操作的原因。
  • 我猜这仅适用于 SB 及以上,因此在 Nehalem 上归零至少需要 1 µop?
  • 是的,归零仅从 SNB 开始“免费”。对于 Merom-Westmere (iirc),它需要一个 µop,但仍然会破坏依赖性(并且它是一个端口 0|1|5 µop,因此除非您的代码非常密集,否则它会在没有任何性能影响的情况下发出)。跨度>
  • @Zboson:另外,您可能对 mips 和 arm64 有一个 named 零寄存器感兴趣($zero on mips,xzr on arm64)。
  • 我认为在您的答案中添加 _mm_set1_epi32 而不是 _mm_setzero_si128 会很有趣。例如,请参阅此问题的两个答案。一个预先定义零和〜零(全一),另一个答案在内在函数中使用它们。哪种方法更好?
【解决方案2】:

关于这个特殊的操作你应该在斯蒂芬佳能说和做

static inline Vec8s operator - (Vec8s const & a) {
    return _mm_sub_epi16(_mm_setzero_si128(), a);
}

直接取自Agner Fog's Vector Class Library

但是让我们考虑一下static 关键字的作用。当您使用static 声明变量时,它使用静态存储。这会将其放置在目标文件的数据部分(包括 .bss 部分)中。

#include <x86intrin.h>
extern "C" void foo2(__m128i a);

static const __m128i zero = _mm_setzero_si128();

static inline __m128i negate(__m128i a) {
    return _mm_sub_epi16(zero, a);
}

extern "C" void foo(__m128i a, __m128i b) {
    foo2(negate(a));
}

我做g++ -O3 -c static.cpp 然后看看拆解和部分。我懂了 有一个带有标签_ZL4zero 的.bss 部分。然后是代码启动部分,将静态变量写入.bss部分。

.text.startup
    pxor    xmm0, xmm0
    movaps  XMMWORD PTR _ZL4zero[rip], xmm0
    ret

foo 函数

    movdqa  xmm1, XMMWORD PTR _ZL4zero[rip]
    psubw   xmm1, xmm0
    movdqa  xmm0, xmm1

所以 GCC 从不为静态变量使用 XMM 寄存器。它从数据部分的内存中读取。

如果我们使用_mm_sub_epi16(_mm_setzero_si128(),a) 会怎样?然后 GCC 生成 foo

    pxor    xmm1, xmm1
    psubw   xmm1, xmm0
    movdqa  xmm0, xmm1

在 Sandy Bridge 之后的 Intel 处理器上,pxor 是“免费的”。在此之前的处理器上,它几乎是免费的。所以这显然是比从内存中读取更好的解决方案。

如果我们尝试_mm_sub_epi16(_mm_set1_epi32(-1),a) 会怎样。在这种情况下,GCC 会产生

    pcmpeqd xmm1, xmm1
    psubw   xmm1, xmm0
    movdqa  xmm0, xmm1

pcmpeqd 指令在任何处理器上都不是free,但它仍然比使用movdqa 从内存中读取要好。好的,所以0-1 很特别。 _mm_sub_epi16(_mm_set1_epi32(1)) 呢?在这种情况下,GCC 生成 foo

    movdqa  xmm1, XMMWORD PTR .LC0[rip]
    psubw   xmm1, xmm0
    movdqa  xmm0, xmm1

这与使用静态变量基本相同!当我查看这些部分时,我发现 .LC0 指向只读数据部分 (.rodata)。

编辑:这是让 GCC 使用 global variable in register 的一种方法。

注册 __m128i 零 asm ("xmm15") = _mm_set1_epi32(1);

这会产生

movdqa  xmm2, xmm15
psubw   xmm2, xmm0
movdqa  xmm0, xmm2

【讨论】:

  • 非常感谢@Zboson 澄清这一点(当然还有@StephenCanon)!我刚刚遇到另一种情况:没有_mm_abs_ps(),如果你想编写一个独立的函数来模拟它,可能最快的方法是在函数内部使用_mm_andnot_ps(signMask, x)__m128 signMask = _mm_set1_ps(-0.0F)', the latter producing 4 times 0x80000000. So probably the fastest way (to avoid multiple movdqa`)将是将掩码传递给我的_mm_abs_ps() 函数?
  • @Ralf 查看我在回答中提到的VCL。查看文件vectorf128.h。搜索abs。如果您没有找到您正在寻找的答案,请在 SO 上发布问题。你应该给斯蒂芬佳能接受的答案。
【解决方案3】:

由于您使用向量来提高效率,因此您的代码存在问题。

未使用常量初始化的静态变量将在运行时初始化。以线程安全的方式。第一次调用内联函数时,会初始化静态变量。在此之后的每一次调用中,都会检查静态变量是否需要初始化。

因此,在每次调用时,都会进行检查,然后从内存中加载。如果您不使用静态变量,则可能只有一条指令创建该值,以及大量优化机会。从内存加载很慢。

您可以拥有任意数量的静态变量。编译器会处理你扔给它的任何东西。

【讨论】:

    【解决方案4】:

    我想我可以在讨论中添加一个有趣的观点,尤其是我对 _mm_abs_ps() 的评论。如果我定义

    static inline __m128 _mm_abs_ps_2(__m128 x) {
      __m128 signMask = _mm_set1_ps(-0.0F);
      return _mm_andnot_ps(signMask, x);
    }
    

    (Agner Fog 的 VCL http://www.agner.org/optimize/#vectorclass 使用整数 set1、强制转换和 AND 操作,但实际上应该是相同的)并在循环中使用该函数

    float *p = data;
    for (int i = 0; i < LEN; i += 4, p += 4)
      _mm_store_ps(p, _mm_abs_ps_2(_mm_load_ps(p)));
    

    那么 gcc (4.6.3, -O3) 足够聪明,可以通过将其移出循环来避免重复执行 _mm_set1_ps:

        vmovaps xmm1, XMMWORD PTR .LC1[rip] # tmp108,
        mov rax, rsp    # p,
    .L3:
        vandnps xmm0, xmm1, XMMWORD PTR [rax]   # tmp102, tmp108, MEM[base: p_54, offset: 0B]
        vmovaps XMMWORD PTR [rax], xmm0 # MEM[base: p_54, offset: 0B], tmp102
        add rax, 16 # p,
        cmp rax, rbp    # p, D.7371
        jne .L3 #,
    .LC1:
        .long   2147483648
        .long   2147483648
        .long   2147483648
        .long   2147483648
    

    因此,在大多数情况下,可能根本不用担心在某个函数中重复将某些 xmm 寄存器设置为常量。

    【讨论】: