优化标签(空结构)函数参数的处理

gez*_*eza 6 c++ arm x86-64 abi calling-convention

在某些情况下,我们使用标签来区分函数。标签通常是一个空结构:

struct Tag { };
Run Code Online (Sandbox Code Playgroud)

假设我有一个使用此标签的函数:

void func(Tag, int a);
Run Code Online (Sandbox Code Playgroud)

现在,我们调用这个函数:

func(Tag(), 42);
Run Code Online (Sandbox Code Playgroud)

并检查生成的 x86-64 反汇编,godbolt

mov     edi, 42
jmp     func(Tag, int)            # TAILCALL
Run Code Online (Sandbox Code Playgroud)

没关系,标签得到了完全优化:没有为其分配寄存器/堆栈空间。

但是,如果我查看其他平台,就会发现该标签存在一些。

在 ARM 上,r0用作标签,并且它被归零(似乎没有必要):

mov     r1, #42
mov     r0, #0
b       func(Tag, int)
Run Code Online (Sandbox Code Playgroud)

对于 MSVC,ecx被用作标记,并且它是从堆栈中“初始化”的(同样,似乎没有必要):

movzx   ecx, BYTE PTR $T1[rsp]
mov     edx, 42                             ; 0000002aH
jmp     void func(Tag,int)                 ; func
Run Code Online (Sandbox Code Playgroud)

我的问题是:是否有一种标签技术在所有这些平台上都得到了同样的优化?


注意:我没有找到 SysV ABI 指定可以在参数传递时优化空类的地方...(甚至,Itanium C++ ABI说:“空类的传递与普通类没有什么不同”。)

Mic*_*zel 2

我认为这里的基本问题是,在生成函数的独立版本时,编译器必须生成可以由任何人根据各自的调用约定从任何地方调用的代码。当在不知道函数定义的情况下生成对函数的调用时,编译器真正知道的是该函数期望根据调用约定进行调用。基于此,除非调用约定指定删除空类型的函数参数,否则编译器通常无法真正优化函数调用中的参数。现在,对于 C++ 编译器来说,当场制定它认为适合给定函数签名的任何调用约定在技术上可能是合法的,除非该函数具有非 C++ 语言链接(例如,函数extern "C")。但实际上,这很可能不会那么简单。首先,您需要一种算法来决定给定函数签名的最佳调用约定通常是什么样子。其次,使用完全相同的标志链接不一定全部由完全相同的编译器的完全相同版本生成的代码的能力,虽然 C++ 标准没有要求,但在实践中可能是相关的。函数调用约定优化当然不是不可能的。但我不知道有任何 C++ 编译器实际上可以做到这一点(在生成目标代码时)。

一种可能的解决方案是,例如,对实际函数实现使用不同的名称,并使用简单的内联包装函数将带有标签类型的调用转换为相应的实现:

struct TagA { };
struct TagB { };

inline void func(int a, TagA)
{
    void funcA(int a);
    funcA(a);
}

inline void func(int a, TagB)
{
    void funcB(int a);
    funcB(a);
}

void call() {
    func(42, TagA());
    func(42, TagB());
}
Run Code Online (Sandbox Code Playgroud)

在这里尝试一下

另请注意,虽然编译器可能会生成类似于初始目标文件中的函数调用,但链接时优化最终可能会删除未使用的参数。至少有一个主要编译器甚至记录了这种行为……