C++ vararg函数的内联

C++ vararg函数的内联,c++,c,variadic-functions,inline-functions,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,

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

例如,编译以下小程序:

#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;
}
#包括
#包括
静态内联无效测试(常量字符*格式,…)
{
va_列表ap;
va_开始(ap,格式);
vprintf(格式,ap);
va_端(ap);
}
int main()
{
测试(“你好%s\n”,“世界”);
返回0;
}

看起来总是会导致(可能被损坏)<代码>测试< /代码>符号出现在可执行文件中(在Mac OS和Linux上,Clang和GCC在C和C++模式下都进行了测试)。如果修改

test()
的签名以获取传递给
printf()
的普通字符串,那么两个编译器都会按照预期从
-O1
向上内联函数


我怀疑这与用来实现瓦拉格的伏都教魔法有关,但这通常是如何实现的对我来说是个谜。有谁能告诉我编译器通常如何实现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函数,因此有可能实现这样的功能。它不会对printf调用执行此操作,因为它会使代码膨胀很多,但默认情况下,我们的编译器会一直内联所有内容,不管它是什么。。。它确实可以工作,但是我们发现在代码中有2-3个printf副本会使它变得非常庞大(还有其他各种各样的缺点,包括由于编译器后端的一些错误算法选择,最终代码生成需要花费更长的时间),所以我们不得不添加代码来阻止编译器这样做

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

一些值自动转换(即:提升)为“较大”类型(这或多或少取决于实现),例如
char
short
被提升为
int
float
double

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

这就是简短的理论

现在,按照惯例,正如上面n.m.的评论所示,gcc不内联具有变量参数处理的函数。可能在处理变量参数时会有相当复杂的操作,这会将代码的大小增加到非最佳大小,因此不值得内联这些函数

编辑:

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

即使使用_forceinline,编译器也无法在所有情况下内联代码。在以下情况下,编译器无法内联函数:

  • 该函数有一个变量参数列表

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

但是对于瓦拉格人来说,一般来说收获很少。
在该函数的正文中考虑这个代码:

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

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

我不希望内联varargs函数,除非是在最简单的情况下

如果varargs函数没有参数,或者没有访问它的任何参数,或者只访问变量参数之前的固定参数,则可以通过将其重写为不使用vararg的等效函数来内联