代码之家  ›  专栏  ›  技术社区  ›  Tristan Brindle

vararg函数的嵌入

  •  19
  • Tristan Brindle  · 技术社区  · 10 年前

    在玩优化设置时,我注意到一个有趣的现象:函数采用可变数量的参数( ... )似乎从未内联。(显然,这种行为是特定于编译器的,但我已经在几个不同的系统上进行了测试。)

    例如,编译以下小程序:

    #include <stdarg.h>
    #include <stdio.h>
    
    static inline void test(const char *format, ...)
    {
      va_list ap;
      va_start(ap, format);
      vprintf(format, ap);
      va_end(ap);
    }
    
    int main()
    {
      test("Hello %s\n", "world");
      return 0;
    }
    

    似乎总是会导致(可能损坏) test 符号出现在生成的可执行文件中(在MacOS和Linux上以C和C++模式使用Clang和GCC进行测试)。如果修改 test() 获取传递给 printf() ,函数内联自 -O1 正如您所期望的那样,两个编译器都向上。

    我怀疑这与用于实现varargs的巫毒魔法有关,但这通常是如何实现的,这对我来说是个谜。有人能告诉我编译器通常如何实现vararg函数,以及为什么这似乎阻止了内联?

    4 回复  |  直到 10 年前
        1
  •  11
  •   Mats Petersson    10 年前

    至少在x86-64上,var_args的传递相当复杂(由于在寄存器中传递参数)。其他架构可能不那么复杂,但很少是微不足道的。特别是,可能需要在获取每个参数时引用堆栈帧或帧指针。这些规则很可能会阻止编译器内联函数。

    x86-64的代码包括将所有整数参数和8个sse寄存器推送到堆栈中。

    这是用Clang编译的原始代码中的函数:

    test:                                   # @test
        subq    $200, %rsp
        testb   %al, %al
        je  .LBB1_2
    # BB#1:                                 # %entry
        movaps  %xmm0, 48(%rsp)
        movaps  %xmm1, 64(%rsp)
        movaps  %xmm2, 80(%rsp)
        movaps  %xmm3, 96(%rsp)
        movaps  %xmm4, 112(%rsp)
        movaps  %xmm5, 128(%rsp)
        movaps  %xmm6, 144(%rsp)
        movaps  %xmm7, 160(%rsp)
    .LBB1_2:                                # %entry
        movq    %r9, 40(%rsp)
        movq    %r8, 32(%rsp)
        movq    %rcx, 24(%rsp)
        movq    %rdx, 16(%rsp)
        movq    %rsi, 8(%rsp)
        leaq    (%rsp), %rax
        movq    %rax, 192(%rsp)
        leaq    208(%rsp), %rax
        movq    %rax, 184(%rsp)
        movl    $48, 180(%rsp)
        movl    $8, 176(%rsp)
        movq    stdout(%rip), %rdi
        leaq    176(%rsp), %rdx
        movl    $.L.str, %esi
        callq   vfprintf
        addq    $200, %rsp
        retq
    

    来自gcc:

    test.constprop.0:
        .cfi_startproc
        subq    $216, %rsp
        .cfi_def_cfa_offset 224
        testb   %al, %al
        movq    %rsi, 40(%rsp)
        movq    %rdx, 48(%rsp)
        movq    %rcx, 56(%rsp)
        movq    %r8, 64(%rsp)
        movq    %r9, 72(%rsp)
        je  .L2
        movaps  %xmm0, 80(%rsp)
        movaps  %xmm1, 96(%rsp)
        movaps  %xmm2, 112(%rsp)
        movaps  %xmm3, 128(%rsp)
        movaps  %xmm4, 144(%rsp)
        movaps  %xmm5, 160(%rsp)
        movaps  %xmm6, 176(%rsp)
        movaps  %xmm7, 192(%rsp)
    .L2:
        leaq    224(%rsp), %rax
        leaq    8(%rsp), %rdx
        movl    $.LC0, %esi
        movq    stdout(%rip), %rdi
        movq    %rax, 16(%rsp)
        leaq    32(%rsp), %rax
        movl    $8, 8(%rsp)
        movl    $48, 12(%rsp)
        movq    %rax, 24(%rsp)
        call    vfprintf
        addq    $216, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
    

    在clang for x86中,它要简单得多:

    test:                                   # @test
        subl    $28, %esp
        leal    36(%esp), %eax
        movl    %eax, 24(%esp)
        movl    stdout, %ecx
        movl    %eax, 8(%esp)
        movl    %ecx, (%esp)
        movl    $.L.str, 4(%esp)
        calll   vfprintf
        addl    $28, %esp
        retl
    

    没有什么能真正阻止上述代码被内联,所以看起来这只是编译器编写者的策略决定。当然,打个电话 printf ,为了代码扩展的成本而优化掉调用/返回对是没有意义的——毕竟printf不是一个小的短函数。

    (在过去一年的大部分时间里,我工作的一个重要部分是在OpenCL环境中实现printf,所以我知道的比大多数人都要多,甚至比大多数人查找到的格式说明符和printf的其他各种棘手部分都要多)

    编辑:我们使用的OpenCL编译器将内联调用var_args函数,因此可以实现这样的功能。它不会对printf进行调用,因为它会使代码非常膨胀,但在默认情况下,我们的编译器始终内联所有内容,无论它是什么,但我们发现,代码中有2-3个printf副本会使其变得非常庞大(还有各种其他缺点,包括由于编译器后端的算法选择不当,最终代码生成需要更长的时间),因此我们必须添加代码以阻止编译器这样做。。。

        2
  •  5
  •   fuz    10 年前

    变量参数实现通常具有以下算法:从堆栈中获取格式字符串之后的第一个地址,在解析输入格式字符串时,将给定位置的值用作所需的数据类型。现在,用所需数据类型的大小递增堆栈解析指针,前进格式字符串并将新位置的值用作所需数据数据类型。。。等等

    某些值会自动转换(即:升级)为“更大”类型(这或多或少取决于实现),例如 char short 升级到 int float double .

    当然,您不需要格式字符串,但在这种情况下,您需要知道传入的参数的类型(例如:所有int,或所有double,或前3个int,然后3个double..)。

    这就是简短的理论。

    现在,就实践而言,正如上面n.m.的注释所示,gcc不内联具有可变参数处理的函数。在处理变量参数时,可能会进行非常复杂的操作,这会将代码的大小增加到非最佳大小,因此根本不值得内联这些函数。

    编辑:

    在使用VS2012进行了快速测试之后,我似乎无法说服编译器使用变量参数内联函数。 无论项目的“优化”选项卡中的标志组合如何,都会调用 test 总是有一个 测验 方法事实上:

    http://msdn.microsoft.com/en-us/library/z8y1yy88.aspx

    说的是

    即使使用__forceinline,编译器也不能在所有情况下内联代码。如果出现以下情况,编译器无法内联函数: ...

    • 该函数有一个变量参数列表。
        3
  •  1
  •   user541686    10 年前

    内联的目的是减少函数调用开销。

    但对于瓦拉格来说,总体而言,几乎没有什么收获。
    在该函数的主体中考虑以下代码:

    if (blah)
    {
        printf("%d", va_arg(vl, int));
    }
    else
    {
        printf("%s", va_arg(vl, char *));
    }
    

    编译器应该如何内联它?这样做需要编译器以正确的顺序推送堆栈上的所有内容 无论如何 ,即使没有调用任何函数。唯一被优化的是一个call/ret指令对(可能还有推送/弹出ebp等等)。内存操作无法优化,参数也无法在寄存器中传递。因此,内联varargs不太可能获得任何显著的结果。

        4
  •  1
  •   david.pfx    10 年前

    我不认为内联varargs函数是可能的,除非是最普通的情况。

    没有参数的varargs函数,或者不访问其任何参数,或者只访问变量之前的固定参数,可以通过将其重写为不使用varargs的等效函数来内联。这是一个微不足道的案例。

    访问其可变参数的varargs函数通过执行 va_start va_arg 宏,它依赖于以某种方式在内存中布置的参数。执行内联以消除函数调用开销的编译器仍然需要创建支持这些宏的数据结构。试图删除所有函数调用机制的编译器也必须分析和优化这些宏。如果varadic函数调用另一个传递va_list作为参数的函数,它仍然会失败。

    我看不出第二种情况的可行路径。