内联vararg函数

时间:2014-08-25 08:39:00

标签: c++ c variadic-functions inline-functions

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

例如,编译以下小程序:

#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 个答案:

答案 0 :(得分:11)

至少在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

在for x86的clang中,它更简单:

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副本使它非常庞大(包括各种其他缺点,包括由于编译器后端的一些错误的算法选择而导致最终代码生成需要更长时间),所以我们不得不将代码添加到STOP编译器这样做......

答案 1 :(得分:5)

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

某些值会自动转换(即:提升)为&#34;更大的&#34;类型(这或多或少取决于实现),例如charshort被提升为intfloat提升为double

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

所以这是简短的理论。

现在,按照惯例,作为上午的评论。如上所示,gcc不具有可变参数处理的内联函数。可能在处理变量参数时会进行相当复杂的操作,这会将代码的大小增加到不合适的大小,因此根本不值得内联这些函数。

编辑:

在使用VS2012进行快速测试之后,我似乎无法说服编译器使用变量参数内联函数。 无论&#34;优化&#34;中的标志组合如何?项目的标签总是有一个test的来电,并且始终有一个test方法。事实上:

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

  

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

     
      
  • 该函数有一个可变参数列表。
  •   

答案 2 :(得分:1)

内联点是它减少了函数调用开销。

但对于varargs来说,一般来说几乎没有什么可收获的 在该函数的主体中考虑此代码:

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

编译器应该如何内联它?这样做需要编译器以正确的顺序推送堆栈中的所有内容,即使没有调用任何函数。唯一能被优化的是一个调用/返回指令对(并且可能推送/弹出ebp和诸如此类的东西)。无法优化存储器操作,并且参数不能在寄存器中传递。因此,你不可能通过内联varargs来获得任何值得注意的东西。

答案 3 :(得分:1)

除了最简单的情况外,我不希望能够内联varargs函数。

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

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

我没有看到第二种情况的可行路径。