我需要在2021年使用_mm256_zeroupper吗?

4

来自Agner Fog的《C++软件优化》:

在某些英特尔处理器上,混合编译具有AVX支持和没有AVX支持的代码时存在问题。从AVX代码转换为非AVX代码时,由于YMM寄存器状态的改变,会产生性能损失。在任何从AVX代码转换为非AVX代码的过渡之前,应调用内联函数_mm256_zeroupper()以避免这种性能惩罚。下列情况下可能需要这样做:

• 如果程序的一部分使用AVX支持编译,而程序的另一部分不使用AVX支持编译,则在离开AVX部分之前调用_mm256_zeroupper()。

• 如果一个函数使用CPU分派在多个版本中进行了编译,包括有和没有AVX支持的版本,则在离开AVX部分之前调用_mm256_zeroupper()。

• 如果一个使用AVX支持编译的代码片段调用来自编译器外库的函数,并且该库没有AVX支持,则在调用库函数之前调用_mm256_zeroupper()。

我想知道什么是“某些英特尔处理器”。具体来说,最近五年内是否有生产这样的处理器。这样我就知道是否为修复缺少的_mm256_zeroupper()调用而迟了。

2个回答

8
TL:DR: 不要手动使用_mm256_zeroupper()内置函数,编译器可以理解SSE/AVX转换的内容,并在需要时为您发出vzeroupper。(包括自动矢量化或使用YMM寄存器扩展memcpy/memset/等操作。)
“一些英特尔处理器”指除了Xeon Phi之外的所有处理器。

Xeon Phi(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指令,这是编译器能够优化包含内部函数的函数并可靠地避免过渡惩罚的唯一明智方式。(特别是考虑到内联)。所有主要的编译器都可以自动向量化和/或使用YMM寄存器内联memcpy/memset/array init,因此无需在此之后跟踪使用vzeroupper

约定是在调用或返回时将CPU置于清洁状态,除了调用通过值(在寄存器中或完全)接受__m256/__m256i/d参数的函数,或者返回这样一个值的情况。目标函数(被调用方或调用方)本质上必须是AVX-aware的,并且希望处于脏-上升状态,因为完整的YMM寄存器作为调用约定的一部分正在使用。

x86-64 System V传递向量寄存器中的向量。Windows vectorcall也是如此,但原始的Windows x64约定(现在被命名为“fastcall”以区别于“vectorcall”)通过隐藏指针将向量按值传递到内存中。(这优化了变参函数,使每个参数始终适合8字节槽。)我不知道编译Windows非vectorcall调用的编译器如何处理这个问题,它们是否假设函数可能查看其参数或至少仍然负责在某个时刻使用vzeroupper。可能是的,但如果您正在编写自己的代码生成后端或手写汇编,请查看您关心的一些编译器实际上执行的操作,如果这种情况与您相关。

一些编译器通过在返回接受矢量参数的函数之前省略vzeroupper来进行优化,因为显然调用者是AVX感知的。而且至关重要的是,显然编译器不应该期望调用像void foo(__m256i)这样的函数会使CPU处于清除状态,因此在这样的函数之后,被调用者仍然需要一个vzeroupper,然后是call printf或其他内容。


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

例如,GCC -mno-vzeroupper / clang -mllvm -x86-use-vzeroupper=0。(默认是-mvzeroupper,按上面描述的行为使用,在需要时使用。)

这在-march=knl(骑士着陆)中被暗示,因为它在Xeon Phi CPU上不需要且非常慢(因此应该积极避免)。

或者如果您使用-mavx -mno-veroupper构建libc(以及任何其他库),则可能需要它。glibc对于像strlen这样的函数有一些手写的asm代码,但其中大多数都有AVX2版本。因此,只要您不在AVX1-only CPU上,就可能根本不使用旧版SSE字符串函数。

对于MSVC编译器,在编译使用AVX指令集的代码时,应该优先使用-arch:AVX。如果没有使用/arch:AVX,混合使用__m128__m256可能会导致转换惩罚。但要注意,该选项甚至会让像_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(SSE2)就没有VEX形式。_mm_sha1rnds4_epu32也没有 - 它最初是在Silvermont系列处理器上引入的,这些处理器直到Gracemont(Alder 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 is not a sensible way to use SHA or cvtpi2ps, just randomly using vpaddb to force some extra register copying.)

戈德堡

# 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会通过使用vcvtdq2ps/vshufps来优化掉对MMX寄存器的使用。但这可能并不总是发生。)

这些都是编译器中应该修复的错误,尽管如果必要,在特定情况下您可以通过使用_mm256_vzeroupper()来解决它们。


通常编译器的启发式方法都能很好地工作;例如,if(a) _mm256... 的汇编块会以 vzeroupper 结尾,如果函数中后续的代码可能有条件地运行普通指令(如 paddb)的遗留 SSE 编码。(这只适用于 MSVC;gcc/clang 要求包含 AVX1/2 指令的函数要使用 __attribute__((target("avx")))"avx2" 进行编译,这使它们可以在函数中的任何位置使用 vpaddb 来代替 _mm_add_epi8。你必须基于每个函数的 CPU 特性进行分支/调度,这是有道理的,因为通常你希望整个循环使用 AVX 或不使用 AVX。)

写信给 Agner,他回复说他将在下一次手动更新中提到编译器可能会自动添加 _mm256_zeroupper。 - Alex Guteniev
@AlexGuteniev:希望他真的会说vzeroupper(汇编指令)是由编译器自动添加的。 _mm256_vzeroupper是内置函数,编译器不是通过转换源代码来工作的,而是通过发出汇编语言来工作的。说_mm256_vzeroupper()自动添加是没有多少意义的,只是编译器足够理解SSE-AVX过渡效果,因此不需要它。 - Peter Cordes
2
禁用自动生成 vzeroupper 有一个很好的理由 - 当您在不同的翻译单元之间调用自己的 AVX 向量化函数时。如果一个函数不接受或返回向量,编译器必须假定它需要遗留 SSE 状态并生成 vzeroupper。在这种情况下,应该禁用自动 vzeroupper 并手动插入内部函数以适用于相关的翻译单元。您可以为其他翻译单元保留启用状态。 - Andrey Semashev
1
@PeterCordes,已经更新了。你可能会失望:编译器可能会自动插入_mm256_zeroupper(),也可能不会。从编译器生成的汇编输出可以看出它是否这样做。 - Alex Guteniev
原始的 repro 包括一个浮点示例,它编译为 SSE,因此甚至不需要混合调试/发布即可重新创建 https://godbolt.org/z/roWxcYrPx。我认为这仍然可能是一个问题,因为未优化的构建性能灾难会与缺少 vzeroupper 相乘,但如果编译器供应商不这样认为,我也没关系。 - Alex Guteniev
显示剩余18条评论

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接