内联vararg函数

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

例如,编译以下小程序:

 #include  #include  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函数,以及为什么这似乎阻止了内联?

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

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

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

一些值自动转换(即:提升)为“更大”类型(这或多或少依赖于实现),例如charshort被提升为intfloat被提升为double

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

所以这是简短的理论。

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

编辑:

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

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

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

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

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

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

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

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

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

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

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

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