在尝试优化设置时,我注意到一个有趣的现象:接受可变数量参数(...
)的函数似乎永远不会内联。(很明显,这种行为是特定于编译器的,但我已经在几个不同的系统上进行了测试。)
例如,编译以下小程序:
#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函数的,为什么这似乎会阻止内联?
发布于 2014-08-25 16:52:46
至少在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函数的调用,因此可以实现这样的功能。对于printf的调用,它不会这样做,因为它会使代码变得非常臃肿,但默认情况下,我们的编译器总是内联所有内容,无论它是什么……它确实起作用了,但我们发现在代码中有2-3个printf副本会使它变得非常庞大(有各种其他缺点,包括由于编译器后端的一些糟糕的算法选择而导致最终代码生成时间更长),所以我们不得不添加代码来阻止编译器这样做……
发布于 2014-08-25 18:04:14
我并不认为内联varargs函数是可能的,除非在最微不足道的情况下。
没有参数的varargs函数,或者不访问其任何参数的varargs函数,或者只访问变量1之前的固定参数的varargs函数,可以通过将其重写为不使用varargs的等效函数来内联。这是一个微不足道的例子。
varargs函数通过执行由va_start
和va_arg
宏生成的代码来访问其可变参数,这两个宏以某种方式依赖于在内存中布局的参数。仅仅为了消除函数调用的开销而执行内联的编译器仍然需要创建数据结构来支持这些宏。试图删除所有函数调用机制的编译器也必须分析和优化这些宏。而且,如果变量函数调用另一个函数,并将va_list作为参数传递,它仍然会失败。
对于第二种情况,我看不到可行的途径。
https://stackoverflow.com/questions/25482031
复制相似问题