您应该通过引用还是通过复制传递 __m128 (和其他寄存器类型)?

scx*_*scx 4 c++ sse simd intrinsics

我很长时间以来一直想知道,在 C++ 中传递寄存器类型的最佳方法是什么?

在我的特定情况下,我有一些抽象层,它们依次调用所需的内在函数。Immintrin 函数按值接受(副本),所以我的猜测是它应该是一个副本。但我想确定一下(并满足我的好奇心)。

又名,

__m128 func(__m128 a, __m128 b) {
    return _mm_something(a, b);
}

// vs.

__m128 func(const __m128& a, const __m128& b) {
    return _mm_something(a, b);
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 8

无论如何,您通常希望这样的函数内联,在这种情况下,它与代码生成无关;按值传递是更简单的语法,所以我推荐它。

但在极少数情况下,您以编译器不能或不会内联1 的方式进行调用,通常按值传递。x86-64 System V 和 Windows 等调用约定vectorcall在向量寄存器(XMM0..7 或 YMM0..7)中传递向量参数。

没有向量调用的 Windows x64 会将语言级别的按值传递转换为 asm 中的按引用传递。(vectorcall如果您有带有 SIMD 参数的非内联函数并且您的目标是 Windows,则更佳。)

如果向量只是从调用者的内存中加载,请考虑传递float *arg 而不是__m128让被调用者执行加载。

Immintrin 函数按值接受(复制)

请注意,它们并不是真正在 asm 中调用的函数;它们通常编译为一条 asm 指令,尽管它们可以得到优化。例如,加载内在函数可以折叠到诸如 之类的指令的内存操作数中addps xmm0, [rdi]


脚注 1:例如,通过函数指针,或在没有 LTO(链接时优化)的构建中指向另一个文件。或者一个大函数,尽管通常矢量化是您在本地执行的操作,对程序的其余部分隐藏,因此您可以使其适应不同的指令集,而无需更改程序在各处使用的类型。但是,您可以拥有一个足够大的函数,以便编译器决定不内联,该函数需要一个向量,因为调用站点已经在向量中具有一个不仅仅来自内存的值。

如果抽象级别阻止编译器实际内联大量小函数,那么这对于 SIMD 循环来说将是一场灾难;不幸的是, x86-64 System V 调用约定没有调用保留的 XMM 寄存器,更不用说 YMM/ZMM 了,因此它必须__m128在每个非内联调用周围将局部变量溢出/重新加载到堆栈。Windows x64 有太多调用保留的 XMM 寄存器,但没有调用保留的 YMM/ZMM。

这就是为什么按 CPU 功能(SSE2、SSE4.1、AVX)进行调度需要针对整个循环,而不是内部循环。当然还要加上 call/ret 开销。