使用 GCC 为 Linux 设备驱动程序编译 Intel AVX 内联

2024-04-25

我在 corei7 上的 ubuntu 上运行 gcc 版本 4.8.2。

从谷歌搜索中找到了有关 AVX 内在函数的信息,但我不确定这组内在函数是否可以用于 Linux 设备驱动程序并进行编译。

如果可以的话,这里的任何人都可以告诉我 makefile 的正确设置是什么,以及在 c 源代码中包含哪些头文件才能使用 gcc 编译此 avx?

Thanks.


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这里很好。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 GCC 为 Linux 设备驱动程序编译 Intel AVX 内联 的相关文章

随机推荐