【问题标题】:Optimized handling of tag (empty struct) function parameters优化标签(空结构)函数参数的处理
【发布时间】:2019-03-22 16:26:09
【问题描述】:

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

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++ arm x86-64 calling-convention abi


【解决方案1】:

我认为这里的基本问题是,在生成函数的独立版本时,编译器必须根据各自的调用约定生成任何人都可以从任何地方调用的代码。当在不知道函数定义的情况下生成对函数的调用时,编译器真正知道的是该函数期望根据调用约定被调用。基于此,除非调用约定指定删除空类型的函数参数,否则编译器通常无法真正优化函数调用中的参数。现在,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());
}

try it out here

另外,请注意,虽然编译器可能会在初始目标文件中生成类似的函数调用,但链接时优化最终可能能够摆脱未使用的参数。至少有一个主要的编译器甚至documents 这样的行为......

【讨论】:

  • 是的,x86-64 System V 调用约定将小型结构打包到最多 2 个寄存器中。我没有意识到空结构可以使用 0 个寄存器,但这与现有规则是一致的。虽然 Windows x64 具有非常严格的 arg -> 寄存器映射以简化可变参数函数(每个 arg 恰好填充一个 8 字节 arg 传递槽),例如XMM0 中的 float 第一个 arg 仍然会将第二个 arg 碰撞到 RDX,即使它是第一个整数 arg。
  • clang 将使用__attribute__((noinline)) 优化传递它知道调用者不会使用的参数。我在玩godbolt.org/z/vur-oK 来添加足够多的 args,以至于没有为Tag 留下寄存器,所以看看它是否占用了内存中的一个插槽,并注意到 clang 之前只复制了 call() 中的 16 个字节的未初始化堆栈内存调用func(),并且不在寄存器中设置整数。 gcc 仅在对非内联函数进行自定义 .constprop 克隆时执行那么多 IPA,但属性 noclone 会不必要地禁用它。
猜你喜欢
  • 2020-01-23
  • 1970-01-01
  • 2019-04-01
  • 2017-12-04
  • 1970-01-01
  • 2017-10-02
  • 1970-01-01
  • 2021-03-11
相关资源
最近更新 更多