内联vararg函数

Tri*_*dle 18 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;
}
Run Code Online (Sandbox Code Playgroud)

似乎总是会test在生成的可执行文件中出现(可能是损坏的)符号(在MacOS和Linux上以C和C++模式使用Clang和GCC进行测试).如果修改了签名test()以获取传递给的普通字符串printf(),那么-O1两个编译器都会按照您的预期向上内联函数.

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

Mat*_*son 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
Run Code Online (Sandbox Code Playgroud)

从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
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

没有什么可以阻止任何上述代码被内联,因此看起来它只是编译器编写器的策略决策.当然,对于类似printf的调用,为代码扩展的成本优化掉调用/返回对是没有意义的 - 毕竟,printf不是一个小的短函数.

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

编辑:我们使用的OpenCL编译器对var_args函数进行内联调用,因此可以实现这样的功能.对于printf的调用,它不会这样做,因为它会使代码非常繁琐,但默认情况下,我们的编译器始终将所有内容都内联,无论它是什么......它确实有效,但我们发现它有代码中的2-3个printf副本使它非常庞大(包括各种其他缺点,包括由于编译器后端的一些错误的算法选择而导致最终代码生成需要更长时间),所以我们不得不将代码添加到STOP编译器这样做......


Fer*_*eak 5

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

某些值自动地转化(即:提升),以"大"类型(这是多还是少的实现依赖),如charshort得到晋升int,并floatdouble.

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

所以这是简短的理论.

现在,对于实践,正如上面的nm中的注释所示,gcc没有内联具有可变参数处理的函数.可能在处理变量参数时会进行相当复杂的操作,这会将代码的大小增加到不理想的大小,因此根本不值得内联这些函数.

编辑:

在使用VS2012进行快速测试后,我似乎无法说服编译器使用变量参数内联函数.无论项目"优化"选项卡中的标志组合如何,总是会有一个调用,test并且始终存在一种test方法.事实上:

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

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

  • 该函数具有可变参数列表.