问题描述
TL;DR; 我正在寻找一种标准的方法来基本上告诉编译器将给定寄存器中发生的任何事情传递给下一个函数。 >
基本上我有一个函数 int bar(int a,int b,int c)
。在某些情况下,c
未使用,我希望能够在 bar
未使用的情况下调用 c
,而无需以任何方式修改 rdx
。
例如如果我有
int foo(int a,int b) {
int no_init;
return bar(a,b,no_init);
}
我希望程序集如下:
对于尾声
jmp bar
或正常通话
call bar
注意:clang
通常会产生我正在寻找的东西。但我不确定在更复杂的函数中是否总是如此,我希望每次构建时都不必检查程序集。
GCC
产生:
对于尾声
xorl %edx,%edx
jmp bar
或正常通话
xorl %edx,%edx
call bar
我可以使用内联汇编获得我想要的结果,即将 foo
(用于尾调用)更改为
int foo(int a,int b) {
asm volatile("jmp bar" : : :);
__builtin_unreachable();
}
编译为只是
jmp bar
我了解 xorl %edx,%edx
对性能的影响尽可能接近 0,但是
我想知道是否有标准方法可以实现这一点。
也就是说,对于任何给定的情况,我都可以找到适合它的破解方法。但这将需要我每次验证程序集。我正在寻找一种方法,您可以基本上告诉编译器“传递寄存器中发生的任何事情”。
查看示例:https://godbolt.org/z/eh1vK8
编辑:这发生在 -O3
集上。
解决方法
我想知道是否有标准方法可以实现这一点。
也就是说,对于任何给定的情况,我都可以找到适合它的破解方法。但那 每次都需要我验证程序集。我正在寻找一个 基本上可以告诉编译器“通过任何 碰巧在注册”。
不,没有在 C 或 C++ 中实现它的标准方法。这两种语言都不涉及任何低级函数调用语义,甚至都不承认 CPU 寄存器的存在,* 并且这两种语言都要求每次函数调用都提供对应于所有非可选参数的参数(其中在 C 中只是“所有声明的参数”)。
例如如果我有
int foo(int a,int b) {
int no_init;
return bar(a,b,no_init);
}
...然后您会获得未定义行为,因为在不确定的情况下使用 no_init
的值。无论任何特定的 C 或 C++ 实现都接受它,根据定义,它是非标准的。
如果你想调用bar()
,但你并不关心传递什么值作为第三个参数,那么为什么不选择一个方便的值来传递呢?零,例如:
return bar(a,0);
*就任一语言标准而言,即使是 register
关键字也无法做到这一点。
请注意,如果被调用的函数确实读取了它的第 3 个参数,那么将其保留为未写入的风险会导致对上次使用的任何 EDX 产生错误依赖。例如,它可能是缓存未命中加载或长链计算的结果。
GCC 小心地异或零以在很多情况下打破错误的依赖关系,例如在 cvtsi2ss
(糟糕的 ISA 设计)或 popcnt
(Sandybridge 系列怪癖)之前。
通常 xor edx,edx 基本上是浪费的 2 字节 NOP,但它确实防止了其他独立依赖链(关键路径)的可能耦合。
如果您确定要挫败编译器保护您免受这种情况的尝试,那么 Nate 的 asm("" :"=r"(var));
是一个很好的方法来实现 _mm_undefined_ps()
的整数版本,它实际上会留下未初始化的寄存器。 (请注意,_mm_undefined_ps
并不能保证不写入 XMM reg;一些编译器会为您进行异或零,而不是完全实现内在旨在允许英特尔编译器的虚假依赖鲁莽。)
将函数转换为具有更小的签名(即更少的参数):
extern int bar(int,int,int);
int foo(int a,int int b) {
return ((int (*)(int,int))bar)(a,b);
}
也许可以为2个参数栏做一个宏,甚至去掉foo
:
extern int bar3(int,int);
#define bar2(a,b) ((int (*)(int,int))bar3)(a,b)
int userOfBar(int a,int b) { return bar2 (a,b); }
奇怪的是,鉴于上面的 gcc 没有触及 %edx,但 clang 确实......哦,好吧。
(仍然不能保证编译器不会触及某些寄存器,不过,这是它的领域。否则,您可以直接在汇编中编写这些函数并避免中间人。)
,在大多数平台上适用于 gcc/clang 的一种方法是这样做
int no_init;
asm("" : "=r" (no_init));
return bar(a,no_init);
这样您就不必就 bar
的原型向编译器撒谎(这可能会破坏一些调用约定),并且您可以欺骗编译器认为 no_init
确实已初始化。
我想知道像安腾这样的体系结构具有“陷阱位”,当访问未初始化的寄存器时会导致错误。此代码在那里可能不安全。
据我所知,没有可移植的方法来获得这种行为,但是您可以 ifdef:
#ifdef __GNUC__
#define UNUSED_INT ({ int x; asm("" : "=r" (x)); x; })
#else
#define UNUSED_INT 0
#endif
// ...
bar(a,UNUSED_INT);
然后,您可以在必要时退回到(无限)效率较低但正确的代码。
它会在 gcc/x86-64 上生成一个裸 jmp
,请参阅 https://godbolt.org/z/d3ordK。在 x86-32 上,它不是最佳选择,因为它推送未初始化的寄存器,而不仅仅是调整 esp
的现有减法。请注意,裸 jmp/call
在 x86-32 上是不安全的,因为第三个堆栈槽可能包含一些重要的东西,并且允许被调用者覆盖它(即使该变量在您想到的路径上未使用,编译器可能会将其用作暂存空间)。
一种可移植的替代方法是将 bar
重写为可变参数。但是,当第三个参数存在时,它需要使用 va_arg
来检索第三个参数,这往往效率较低。