代码之家  ›  专栏  ›  技术社区  ›  geza

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

c++
  •  2
  • geza  · 技术社区  · 6 年前

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

    struct Tag { };
    

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

    void func(Tag, int a);
    

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

    func(Tag(), 42);
    

    并查看最终的x86-64反汇编, godbolt :

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

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

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

    挽着胳膊, 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
    

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


    Itanium C++ ABI 上面写着:“空课的通过方式与普通课没有区别”。)

    0 回复  |  直到 6 年前
        1
  •  2
  •   Michael Kenzel    6 年前

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