You can't safely compile a whole file with -march=sandybridge https://stackoverflow.com/questions/61528554/generate-and-optimize-fp-simd-code-in-the-linux-kernel-on-files-which-contains or other options that imply -mavx
and -msse
. That would let GCC use AVX instructions everywhere in functions in this file, including before kernel_fpu_begin()
or after kernel_fpu_end()
1. It might for example use pxor xmm0,xmm0
/ vmovups [rsp], xmm0
to zero some stack memory for local vars, especially a struct initializer. This would silently corrupt user-space FP/SIMD state https://stackoverflow.com/questions/15883947/why-am-i-able-to-perform-floating-point-operations-inside-a-linux-kernel-module.
但对于 GCC/clang,你不能使用内在函数或__builtin_ia32
您尚未告诉编译器目标支持的指令的函数。编译器将拒绝发出此类指令。 (MSVC 和 ICC 遵循不同的设计理念,它们通常不optimize内在函数,但您可以在任何地方使用它们。)
内联 asm 是不同的:然后您直接将文本打印到编译器的 asm 输出中,除了您通过约束告诉它的内容之外,它不知道该输出。 (对于 GCC 来说确实如此,并且带有内置汇编器的 clang 允许同样绕过目标选项。)这就是为什么内核代码通常使用内联汇编而不是使用目标选项和内在函数。 (还因为它通常用于一个手动调整的循环。)
脚注1:除非此文件仅包含已完成的使用 kernel_fpu_begin() 调用的函数,即您将内部使用函数放在与执行此操作的文件不同的文件中kernel_fpu_begin();
bar_avx();
kernel_fpu_end();
。但这很不方便,而且 GNU C 还有其他方法来设置基于每个函数的目标 ISA 扩展选项。
安全的方法:__attribute__((target("foo")))
你什么can我认为安全地做的是编写一个函数__attribute__((target("avx,sse")))
,并且仅在之间调用它kernel_fpu_begin()
/ kernel_fpu_end()
. 函数无法内联到具有不同目标选项的调用者中,因此请确保它contains一个循环;不要在紧密循环中调用它.
至少对于现代 GCC/clang(GCC7 及更高版本),#include <immintrin.h>
将定义所有函数和类型,即使您在以下情况下包含它:__AVX__
未定义,因此您可以将其包含在使用以下命令编译的文件中-mno-sse
就像通常的内核代码一样,然后在 AVX2 函数中使用 SSE / AVX2 内在函数。你不需要#pragma GCC target("avx2,bmi2")
在包含之前,即使它需要定义一些返回的内联函数__m256i
.
对于 GCC6 及更早版本,#include <immintrin.h>
打破与-mno-sse -mno-mmx
, but #pragma GCC target("sse,mmx,avx2,bmi2")
修复它,定义所有必要的 AVX2 类型和函数。https://godbolt.org/z/M4sEs61EG https://godbolt.org/z/M4sEs61EG。在早期的 clang 中,我有时设法让它发出 SIMD 内在函数的标量模拟,但这没有帮助。使用 clang7 或更高版本。
上的示例Godbolt 编译器浏览器 https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:%271%27,fontScale:14,fontUsePx:%270%27,j:1,lang:___c,selection:(endColumn:42,endLineNumber:22,positionColumn:42,positionLineNumber:22,selectionStartColumn:42,selectionStartLineNumber:22,startColumn:42,startLineNumber:22),source:%27void+kernel_fpu_begin(void)%3B+++//+stub+declarations+for+a+stand-alone+test+file%0Avoid+kernel_fpu_end(void)%3B%0A%0Astatic+void+bar_avx(int+*p,+unsigned+long+len)%3B%0Avoid+foo(int+*p,+unsigned+long+len)%0A%7B%0A++++kernel_fpu_begin()%3B%0A++++bar_avx(p,+len)%3B+++//+can!%27t+inline+because+it+has+different+target+options+than+this+caller%0A++++kernel_fpu_end()%3B%0A%7D%0A%0A%23ifdef+__AVX__%0Aint+avx_defined_before_bar+%3D+1%3B%0A%23else%0Aint+avx_defined_before_bar+%3D+0%3B%0A%23endif%0A%0A//%23pragma+GCC+target(%22avx2,bmi2%22)++%0A+//+this+alone+is+sufficient+for+GCC,+but+clang+ignores+it%0A%23include+%3Cimmintrin.h%3E+%0A//+apparently+target+options+*don!%27t*+need+to+be+in+effect+when+you+%23include+immintrin.h%0A//+with+GCC7+and+later,+and+also+clang7.0%0A%0A//__attribute__((target(%22avx2,bmi2,arch%3Dhaswell%22)))++//+breaks+with+GCC,+fine+with+clang%0A__attribute__((target(%22avx2,bmi2%22)))+++//+works+with+both+GCC+and+clang%0Astatic+void+bar_avx(int+*p,+unsigned+long+len)%7B%0A++++__m256i+v+%3D+_mm256_loadu_si256(+(__m256i*)p+)%3B%0A++++v+%3D+_mm256_slli_epi32(v,+2)%3B+++++//+left+shift+8+ints+by+2%0A++++_mm256_storeu_si256((__m256i*)p,+v)%3B%0A++++p%5B10%5D+%3D+_pext_u64(len,+len)%3B+++++//+collect+set+bits+at+the+bottom.%0A%7D%0A%27),l:%275%27,n:%270%27,o:%27C+source+%231%27,t:%270%27)),k:44.729267853930565,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((g:!((h:compiler,i:(compiler:cg112,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,libraryCode:%271%27,trim:%271%27),flagsViewOpen:%271%27,fontScale:14,fontUsePx:%270%27,j:1,lang:___c,libs:!(),options:%27-O2+-mno-vzeroupper+-mno-avx+-mno-sse+-mno-mmx+-Wall+-mcmodel%3Dkernel+-ffreestanding%27,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:%271%27),l:%275%27,n:%270%27,o:%27x86-64+gcc+11.2+(C,+Editor+%231,+Compiler+%231)%27,t:%270%27)),k:55.270732146069435,l:%274%27,m:49.027635619242574,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:cclang1300,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,libraryCode:%271%27,trim:%271%27),flagsViewOpen:%271%27,fontScale:14,fontUsePx:%270%27,j:2,lang:___c,libs:!(),options:%27-O2+-mno-vzeroupper+-mno-sse+-mno-avx+-mno-mmx+-Wall+-mcmodel%3Dkernel+-ffreestanding%27,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:%271%27),l:%275%27,n:%270%27,o:%27x86-64+clang+13.0.0+(C,+Editor+%231,+Compiler+%232)%27,t:%270%27)),l:%274%27,m:50.972364380757426,n:%270%27,o:%27%27,s:0,t:%270%27)),k:55.270732146069435,l:%273%27,n:%270%27,o:%27%27,t:%270%27)),l:%272%27,n:%270%27,o:%27%27,t:%270%27)),version:4(还有一些关于编译指示的额外注释,以及//__attribute__((target("avx2,bmi2,arch=haswell")))
显然正在使用 clang,但不是 GCC,不知道为什么。我希望它也能设置调整选项,因为tune=generic做出了糟糕的选择,将未对齐的 256 位加载/存储与许多 GCC 版本分开 https://stackoverflow.com/questions/52626726/why-doesnt-gcc-resolve-mm256-loadu-pd-as-single-vmovupd。您可以放心使用-mtune=haswell
or tune=intel
对于整个文件(如有必要)。 (这对于 Zen1 来说应该还不错,对于 Zen2/3 来说可能也不错。)
void kernel_fpu_begin(void); // stub declarations for a stand-alone test file
void kernel_fpu_end(void);
static void bar_avx(int *p, unsigned long len);
void foo(int *p, unsigned long len)
{
kernel_fpu_begin();
bar_avx(p, len); // can't inline because it has different target options than this caller
kernel_fpu_end();
}
#include <immintrin.h>
__attribute__((target("avx2,bmi2"))) // works with both GCC and clang
static void bar_avx(int *p, unsigned long len){
__m256i v = _mm256_loadu_si256( (__m256i*)p );
v = _mm256_slli_epi32(v, 2); // left shift 8 ints by 2
_mm256_storeu_si256((__m256i*)p, v);
p[10] = _pext_u64(len, len); // collect set bits at the bottom.
}
使用 gcc11.2 编译为以下 asm-O2 -mno-vzeroupper -mno-avx -mno-sse -mno-mmx -Wall -mcmodel=kernel -ffreestanding
. (Not exactlyLinux 使用什么,但完全禁用所有 MMX、SSE 和 AVX 代码生成。大概-mno-avx
是多余的-mno-sse
.)
bar_avx:
vmovdqu ymm1, YMMWORD PTR [rdi]
pext rsi, rsi, rsi
mov DWORD PTR [rdi+40], esi
vpslld ymm0, ymm1, 2
vmovdqu YMMWORD PTR [rdi], ymm0
# vzeroupper present without -mno-vzeroupper. But not needed because kernel_fpu_end is about to xrstor and replace the current YMM state
ret
foo:
push r12
mov r12, rsi
push rbp
mov rbp, rdi # save the incoming args in call-preserved regs
sub rsp, 8 # align the stack
call kernel_fpu_begin
mov rsi, r12
mov rdi, rbp
call bar_avx
add rsp, 8 # epilogue restoring stack and saved regs
pop rbp
pop r12
jmp kernel_fpu_end # tailcall
AVX 指令的所有使用都包含在 kernel_fpu_begin/end 之间。
当然,我没有做任何会诱使编译器使用 SIMD 指令的事情,例如对数组或结构进行零初始化。但事实是bar_avx()
没有内联是非常明显的证据,表明 GCC 和 clang 由于具有不同的目标选项而将该函数分开。他们不知道如何在具有不同目标选项的不同块的单个函数内进行优化,因此他们不需要内联。bar_avx()
非常小,并且肯定会正常内联,特别是当它是static
因此它不需要发出它的独立副本。
整数内在函数:
You can安全地使用仅操作通用整数寄存器的内在函数,例如_popcnt_u32
或体重指数2_pdep_u64
,只要您启用适当的 CPU 功能,例如-mpopcnt
and -mbmi2
分别。但请确保不要间接启用 SSE,例如-msse4.2
or -march=haswell
会做。
您甚至不需要 kernel_fpu_begin/end 围绕这些,因为它们只使用通用整数寄存器,与指令相同add
and imul
.
编译整个内核是安全的-mbmi -mbmi2 -mpopcnt
,只要你不关心它运行在Haswell/Excavator之前的CPU上,或者Intel Pentium/Celeron CPU上。 (至少在 Ice Lake 之前,英特尔在 i3 以下的低端 CPU 上禁用了 VEX 前缀解码,因此这意味着禁用使用该整数指令编码的 BMI1 和 BMI2。)
但是,如果您想使用运行时 CPU 检测来避免在不支持它们的 CPU 上执行它们,那么您将再次需要使用__attribute__((target("bmi2")))
在某些功能上。如果你编译了整个文件-mbmi2
, GCC 可能决定使用shlx
or shrx
例如,对于 CPU 检测块之外的某些变量计数移位。
Related:
-
x86 SIMD 内在函数的头文件 https://stackoverflow.com/questions/11228855/header-files-for-x86-simd-intrinsics/31185861#31185861 - immintrin.h
对于大多数事情,英特尔在其文档中记录了内在指南 https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html,就像 ISA 扩展一样。
x86intrin.h
(有点相当于 MSVCintrin.h
)为此以及更多,包括类似的东西_bit_scan_forward
and rdtsc
那些不在immintrin.h
-
为什么我能够在 Linux 内核模块内执行浮点运算? https://stackoverflow.com/questions/15883947/why-am-i-able-to-perform-floating-point-operations-inside-a-linux-kernel-module
-
为什么 gcc 不将 _mm256_loadu_pd 解析为单个 vmovupd? https://stackoverflow.com/questions/52626726/why-doesnt-gcc-resolve-mm256-loadu-pd-as-single-vmovupd不要使用_mm256_loadu_pd
如果您不希望 GCC 将其悲观化,则无需 GCC 上的调整选项vmovups xmm
/ vinsertf128
与曲调=通用。
-
GCC 手册重新:target属性 https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-target-function-attribute(与 GCC 和 clang 一起使用)
-
and #pragma GCC target("avx,sse") https://gcc.gnu.org/onlinedocs/gcc/Function-Specific-Option-Pragmas.html#index-pragma-GCC-target(clang不知道这个)
-
如果您的程序+库不包含 SSE 指令,那么使用 VZEROUPPER 有用吗? https://stackoverflow.com/questions/49019614/is-it-useful-to-use-vzeroupper-if-your-programlibraries-contain-no-sse-instruct关于用户空间中的 vzeroupper 的一些疯狂猜测可能会使上下文切换更便宜。很确定它在这里无关紧要,因为kernel_fpu_end
is not将保存内核留下的当前 FPU 状态,只需用用户空间状态覆盖它即可。在此之前没有任何东西可以运行旧版 SSE 指令xrstor
or xrstors
. So -mno-vzeroupper
这里很好。