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]);
__m128i t2 = _mm_sha1rnds4_epu32(t1,t1, 3);
t1 = _mm_add_epi8(t1,t2);
_mm_storeu_si128((__m128i*)&dst[32], t1);
#endif
#ifdef __MMX__
__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。)
vzeroupper
(汇编指令)是由编译器自动添加的。_mm256_vzeroupper
是内置函数,编译器不是通过转换源代码来工作的,而是通过发出汇编语言来工作的。说_mm256_vzeroupper()
被自动添加是没有多少意义的,只是编译器足够理解SSE-AVX过渡效果,因此不需要它。 - Peter Cordesvzeroupper
有一个很好的理由 - 当您在不同的翻译单元之间调用自己的 AVX 向量化函数时。如果一个函数不接受或返回向量,编译器必须假定它需要遗留 SSE 状态并生成vzeroupper
。在这种情况下,应该禁用自动vzeroupper
并手动插入内部函数以适用于相关的翻译单元。您可以为其他翻译单元保留启用状态。 - Andrey Semashevvzeroupper
相乘,但如果编译器供应商不这样认为,我也没关系。 - Alex Guteniev