c++ 2021年我需要使用_mm256_zeroupper吗?

mhd8tkvw  于 2022-12-30  发布在  其他
关注(0)|答案(2)|浏览(307)

Agner Fog's "Optimizing software in C++"开始:
在某些英特尔处理器上混合使用AVX支持和不使用AVX支持编译的代码时会出现问题。由于YMM寄存器状态的变化,从AVX代码转换为非AVX代码时会导致性能损失。在从AVX代码转换为非AVX代码之前,应通过调用内部函数_mm256_zeroupper()来避免这种损失。在以下情况下可能需要这样做:
·如果程序的一部分是使用AVX支持编译的,而程序的另一部分是在不使用AVX支持的情况下编译的,则在离开AVX部分之前调用_mm256_zeroupper()。
·如果使用CPU调度在多个版本中编译函数,无论是否使用AVX,请在离开AVX部分之前调用_mm256_zeroupper()。
·如果使用AVX支持编译的一段代码调用编译器自带库以外的库中的函数,并且该库不支持AVX,则在调用库函数之前调用_mm256_zeroupper()。
我想知道什么是 * 一些英特尔处理器 *。具体来说,有没有处理器在过去五年。所以我知道是否为时已晚,以修复丢失的_mm256_zeroupper()调用或没有。

sr4lhrrt

sr4lhrrt1#

TL:DR:不要手动使用_mm256_zeroupper()intrinsic,编译器理解SSE/AVX转换的内容,并在需要的地方发出vzeroupper。(包括自动矢量化或扩展memcpy/memset/YMM regs时)
“部分英特尔处理器”是指除至强融核之外的所有处理器。
至强融核(KNL / KNM)没有针对运行传统SSE指令而优化的状态,因为它们纯粹是为运行AVX-512而设计的。传统SSE指令可能总是有错误的依赖项合并到目标中。
在带有AVX或更高版本的主流CPU上,有两种不同的机制:节省肮脏的鞋面(SnB通过Haswell和Ice Lake)或虚假依赖(Skylake)。请参阅Why is this SSE code 6 times slower without VZEROUPPER on Skylake?两种不同风格的SSE/AVX惩罚
有关asm vzeroupper(在编译器生成的机器码中)影响的相关问答:

C或C++源代码中的内部函数

在C/C++源代码中绝对不要使用_mm256_zeroupper()。我们已经决定让编译器在需要的地方自动插入vzeroupper指令,这几乎是编译器能够优化包含intrinsic的函数并仍然可靠地避免转换损失的唯一明智的方法。所有主流编译器都可以自动向量化和/或使用YMM寄存器内联memcpy/memset/array init,因此需要跟踪在此之后使用vzeroupper的情况。
约定是在调用或返回时使CPU处于clean-uppers状态,除非调用按值接受__m256/__m256i/d参数的函数(在寄存器中或根本没有),或返回这样的值时。目标函数(被调用方或调用方)本质上必须是AVX感知的,并且预期脏上状态,因为满的YMM寄存器处于-用作调用约定的一部分。

x86-64 System V在向量寄存器中传递向量。Windows vectorcall也是这样,但最初的Windows x64约定(现在命名为“fastcall”以区别于“vectorcall”)通过隐藏指针在内存中按值传递向量。(这通过使每个arg始终适合8字节槽来优化可变变量函数。)IDK编译Windows非vectorcall调用的编译器如何处理这个问题,他们是否假设函数可能会查看它的args,或者至少在某些时候仍然负责使用vzeroupper,即使它不这样做。可能是的,但是如果您正在编写自己的代码生成后端,或者手写的asm,如果这种情况与您相关,请查看您关心的一些编译器实际上是如何做的。
一些编译器在从一个带向量参数的函数返回之前也会省略vzeroupper,因为调用者显然是支持AVX的。而且关键的是,显然编译器不应该期望调用像void foo(__m256i)这样的函数会让CPU处于clean-upper状态,所以被调用者在这样的函数之后、call printf之前或其他地方仍然需要一个vzeroupper

编译器具有控制vzeroupper使用的选项

例如,GCC -mno-vzeroupper/clang-mllvm -x86-use-vzeroupper=0(默认值为-mvzeroupper,用于执行上述操作,在需要时使用)。
-march=knl(Knight 's Landing)暗示了这一点,因为它在至强融核CPU上不需要,而且速度非常慢(因此应该积极避免)。
或者如果你用-mavx -mno-veroupper构建libc(和你使用的任何其他库),你可能需要它。glibc有一些手写的asm函数,比如strlen,但是大多数都有AVX 2版本。所以只要你不是在一个只有AVX 1的CPU上,字符串函数的传统SSE版本可能根本就用不到。
对于MSVC,当编译使用AVX内部函数的代码时,你肯定会更喜欢使用-arch:AVX。我认为如果你混合了__m128__m256而没有/arch:AVX,MSVC的一些版本可能会生成导致转换损失的代码。但是要注意,该选项甚至会使像_mm_add_ps这样的128位内部函数使用AVX编码(vaddps)而不是传统SSE(addps),并允许编译器使用AVX自动矢量化。有一个未公开的开关/d2vzeroupper可启用自动vzeroupper生成(默认),/d2vzeroupper-禁用它-请参见What is the /d2vzeroupper MSVC compiler optimization flag doing?

MSVC和GCC/clang可能被诱骗执行传统SSE编码,该编码写入带有脏上限的XMM寄存器:

编译器启发式可能会假设有一个VEX编码可用于一个函数中的任何指令,该函数肯定(无条件地)已经执行了AVX指令。有些,如cvtpi2ps xmm, mm(MMX+SSE)或movqd2d xmm, mm(SSE 2)没有VEX格式,也没有_mm_sha1rnds4_epu32-它是在Silvermont-family上首次引入的,该fix直到Gracemont(桤木Lake)才支持AVX,因此它是以128位非VEX编码引入的,但仍然没有VEX编码。

#include <immintrin.h>

void bar(char *dst, char *src)
{
      __m256 vps = _mm256_loadu_ps((float*)src);
      _mm256_storeu_ps((float*)dst, _mm256_sqrt_ps(vps));

#if defined(__SHA__) || defined(_MSC_VER)
        __m128i t1 = _mm_loadu_si128((__m128i*)&src[32]);
                 // possible MSVC bug, writing an XMM with a legacy VEX while an upper might be dirty
        __m128i t2 = _mm_sha1rnds4_epu32(t1,t1, 3);  // only a non-VEX form exists
        t1 = _mm_add_epi8(t1,t2);
        _mm_storeu_si128((__m128i*)&dst[32], t1);
#endif
#ifdef __MMX__  // MSVC for some reason dropped MMX support in 64-bit mode; IDK if it defines __MMX__ even in 32-bit but whatever
        __m128 tmpps = _mm_loadu_ps((float*)&src[48]);
        tmpps = _mm_cvtpi32_ps(tmpps, *(__m64*)&src[48]);
        _mm_storeu_ps((float*)&dst[48], tmpps);
#endif

}

(This使用SHA或cvtpi2ps并不明智,只是随机使用vpaddb强制执行一些额外的寄存器复制。)

一个月一次

# clang -O3 -march=icelake-client
bar(char*, char*):
        vsqrtps ymm0, ymmword ptr [rsi]
        vmovups ymmword ptr [rdi], ymm0   # first block, AVX1

        vmovdqu xmm0, xmmword ptr [rsi + 32]
        vmovdqa xmm1, xmm0
        sha1rnds4       xmm1, xmm0, 3     # non-VEX encoding while uppers still dirty.
        vpaddb  xmm0, xmm1, xmm0
        vmovdqu xmmword ptr [rdi + 32], xmm0

        vmovups xmm0, xmmword ptr [rsi + 48]
        movdq2q mm0, xmm0
        cvtpi2ps        xmm0, mm0         # again same thing
        vmovups xmmword ptr [rdi + 48], xmm0
        vzeroupper                        # vzeroupper not done until here, too late for code in this function.
        ret

MSVC和GCC基本相同(尽管GCC在这种情况下优化了MMX寄存器的使用,使用vcvtdq2ps/vshufps,但这种情况可能不会经常发生)。
这些是编译器错误,应该在编译器中修复,但是如果需要,您可以在特定情况下使用_mm256_vzeroupper()解决这些错误。
通常情况下,编译器启发式工作良好;例如,如果函数中后面的代码可能有条件地运行常规指令(如paddb)的传统SSE编码,则if(a) _mm256...的asm块将以vzeroupper结束。(这仅在MSVC中可能; gcc/clang要求使用__attribute__((target("avx")))"avx2"编译包含AVX 1/ 2指令的函数,这使得它们可以在函数中的任何地方使用vpaddb作为_mm_add_epi8。您必须在每个函数级别上基于CPU特性进行分支/分派,这是有意义的,因为通常您希望使用AVX运行整个循环。)

kgqe7b3p

kgqe7b3p2#

AVX -〉SSE惩罚(不归零)适用于当前处理器。参见Intel® 64 and IA-32 Architectures Optimization Reference Manual, June 2021
然而,在C/C++代码中缺少_mm256_zeroupper()并不一定是个问题。编译器可能会自己插入它。所有编译器都会:https://godbolt.org/z/veToerhvG
实验表明,自动vzeroupper插入在VS 2015中有效,但在VS 2012中无效

相关问题