所有YMM寄存器都被呼叫阻塞
,因此,非内联函数有点像是在寄存器中保留任何大量数据的挡箭牌。(Windows x64约定保留了xmm6..15的调用,但较宽的YMM寄存器仍然被阻塞。)相当多的整数寄存器也被阻塞,尤其是在x86-64 System V调用约定(非Windows)中。
如果你的程序的有价值的状态只有这4个矢量和几个整数寄存器,那么是的,MSVC的x64
vectorcall
可以将向量传递给非内联函数,并将它们全部作为返回值返回。
否则,其他状态将不得不在调用周围溢出/重新加载,因此手工编写的asm的唯一好选择是GNUC内联asm。
x86-64 SysV在x/y/zmm0中返回1个矢量
这个
x86-64 System V calling convention
最多可以在2个矢量寄存器(xmm/ymm/zmm)中返回,就像整数参数可以在最多6个regs中传递,但只能在RDX:RAX中返回一样。
但XMM1仅在返回标量float或double的聚合时使用(总大小不超过16字节,因此返回值在XMM0和XMM1的低八字节中)。ABI文档的分类规则5(c)-
如果聚合的大小超过两个八字节,而第一个八字节不是
SSE或任何其他八字节都不是SSEUP,整个参数在内存中传递。
-一秒钟
__m128i
结构中的矢量将具有第二个SSE分类的八字节。这就是为什么这样的结构在内存中返回,而不是XMM0、XMM1。规则5c允许在YMM0或ZMM0中返回比16字节宽的单个矢量(其中后面的所有八个字节都是SSEUP),而不是其他情况。
测试证实了这一点。具有
struct { __m256i v[2]; }
,GCC/clang在内存中返回,而不是YMM0/YMM1,请参阅下面的Godbolt链接。但是
struct { float v[3]; }
我们看到了
v[4]
在XMM1的元素1中返回(低64位的上半部分=八字节):
Godbolt
因此,AMD64 System V ABI的调用约定不适合您的用例,即使它可以在向量regs中返回2个向量。
vectorcall
在GCC或clang中:与MSVC不同,只有1个矢量reg
您可以使用声明asm函数的原型
__attribute__((ms_abi))
(gcc或clang)或
__attribute__((vectorcall))
(仅限于叮当声),但这实际上似乎并不像你描述MSVC工作的方式:一个多个的结构
__m256i
通过隐藏指针在内存中返回,即使使用
vectorcall
(
Godbolt
)
Agner Fog对GCC错误报告的评论(
89485
)说clang针对Windows确实支持
__vectorcall
,但那个bug只是请求GCC支持它,而不是讨论它是否在寄存器中返回了多个向量。也许clang的实现
__vectorcall
ABI对于多个向量的结构返回不与MSVC兼容吗?
我没有Windows clang可供测试,也没有clang cl,它旨在与MSVC更兼容。
asm("call foo" : "+x"(v0), ...);
包装器也不会碰撞其他regs
正如你在评论中所建议的
能够
发明自己的调用约定,并通过内联asm向编译器描述它。只要它是一个纯函数,您甚至可以避免
"memory"
撞击。
您确实需要停止编译器在调用程序中使用红色区域,因为
call
推送返回地址。看见
Inline assembly that clobbers the red zone
编译器根本不知道这是一个函数调用
;事实上,内联asm模板恰好在堆栈上推送/弹出一些东西,这是重要的一部分,而不是在执行从另一边出来之前它会跳到其他地方。编译器不解析asm模板字符串,只是替换
%operand
s、 像printf。它不在乎是否显式引用操作数。
所以你仍然有内联asm的所有优点和缺点(
https://gcc.gnu.org/wiki/DontUseInlineAsm
),包括必须精确地描述输出:输入:对正在运行的代码块的编译器的破坏,比如如何在注释中为手工编写的asm-helper函数进行文档记录。
加上的开销
呼叫
和
ret
与将你的asm写在asm语句本身中相比。
对于两个这么便宜的东西来说,这似乎很糟糕
vpermq
说明书你也许可以使用
asm(".include 'helper.s'" : "+x"(v0), ...);
如果你能把你的助手分成一个文件。(或者也许
.set
一个
.if
可以检查,这样你就可以从一个有多个块的文件中要求一个块?但这可能更难维持。)
如果您正在使用
"m"
可能选择相对于RSP的寻址模式的操作数,也可能中断为
呼叫
推送返回地址。但你不会在这种情况下;您将强制编译器为操作数选择特定的寄存器,而不是让它选择要选择的YMM寄存器。
所以它可能看起来像
#include <immintrin.h>
auto bar(__m256i v0_in, __m256i v1_in, __m256i v2_in, __m256i v3_in){
// clang does pass args in the right regs for vectorcall
// (after taking into account that the first arg-reg slot is taken by the hidden pointer because of disagreement about aggregate returns)
register __m256i v0 asm("ymm0") = v0_in; // force "x" constraints to pick a certain register for asm statements.
register __m256i v1 asm("ymm1") = v1_in;
register __m256i v2 asm("ymm2") = v2_in;
register __m256i v3 asm("ymm3") = v3_in;
v1 = _mm256_add_epi64(v1, v3); // do something with the incoming args, just for example
__m256i vlocal = _mm256_add_epi64(v0, v2); // compiler can allocate this anywhere
// declare some integer register clobbers if your function needs any
// the fewer the better; the compiler can keep its own stuff in those regs otherwise
asm("call asm_foo" : "+x"(v0), "+x"(v1), "+x"(v2), "+x"(v3) : : "rax", "rcx", "rdx");
// if you don't compile with -mno-red-zone, then "add $-128, %%rsp ; call ; sub $-128, %%rsp".
// But you don't want that each call inside a loop, so just use -mno-red-zone
return _mm256_add_epi64(vlocal, v2);
}
Godbolt
gcc和clang将其编译为:
# clang16 -O3 -march=skylake -mno-red-zone
bar(long long __vector(4), long long __vector(4), long long __vector(4), long long __vector(4)):
vpaddq ymm1, ymm3, ymm1
vpaddq ymm4, ymm2, ymm0 # compiler happened to pick ymm4 for vlocal, a reg not clobbered by the asm statement.
# inline asm starts here
call asm_foo
# inline asm ends here
# if we just return v2, we get vmovaps ymm0, ymm2
vpaddq ymm0, ymm4, ymm2 # use ymm4 which was *not* clobbered by the inline asm statement,
# along with the v2 = ymm2 output of the asm
ret
与GCC在处理寄存器分配的硬寄存器约束方面一如既往的糟糕相比:
# gcc13 -O3 -march=skylake -mno-red-zone
bar(long long __vector(4), long long __vector(4), long long __vector(4), long long __vector(4)):
vmovdqa ymm5, ymm2 # useless copies, silly compiler.
vmovdqa ymm4, ymm0
vpaddq ymm1, ymm1, ymm3
vpaddq ymm4, ymm4, ymm5
call asm_foo
vpaddq ymm0, ymm4, ymm2
ret
无论你在
asm_foo
函数,您也可以在asm模板中完成。然后你可以使用
%0
而不是
%%ymm0
为编译器提供寄存器的选择。我将变量与传入的args排成一行,以便于编译器使用。
asm_fo
是具有特殊调用约定的函数。
bar()
只是一个普通函数,它的调用方将假定会阻塞所有向量regs和一半整数regs,并且只能按值返回一个向量。