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

Optimized handling of tag (empty struct) function parameters

本文关键字:函数 参数 处理 结构 标签 优化      更新时间:2023-10-16

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

struct Tag { };

假设我有一个函数,它使用这个标签:

void func(Tag, int a);

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

func(Tag(), 42);

并查看由此产生的 x86-64 拆卸,godbolt:

mov     edi, 42
jmp     func(Tag, int)            # TAILCALL

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

但是,如果我查看其他平台,该标签有一些存在感。

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

mov     r1, #42
mov     r0, #0
b       func(Tag, int)

使用 MSVC,ecx用作标记,并从堆栈"初始化"(同样,似乎没有必要):

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

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


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

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

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

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());
}

在这里尝试一下

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