编译时常量和形式参数

Ars*_*sen 2 c compile-time-constant

在以下示例中:

static inline void foo(const int varA)
{
  ...
  __some_builtin_function(varA);
  ...
}

int main()
{
  foo(10);
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

这里的 varA 被视为编译时常量吗?

请注意,我使用的是 C,而不是 C++。

任何到标准或描述编译时常量的可靠文档的链接,特别是它们与形式参数的关系,将不胜感激。

Bee*_*ope 5

不,varA不是编译时常量 - 每次调用函数时它肯定会不同。常量在标准中有具体的定义 -这个答案中涉及了一些关键细节,或者您可以阅读官方单词的标准。

也就是说,您可能想知道的是,在您像示例中那样使用常量值调用它的情况下,编译器是否会将其视为常量。对于任何开启优化的体面编译器来说,答案是“是”。调用内联和持续传播是实现这一点的魔力。编译器将尝试内联调用foo,然后替换10参数,并将递归地遵循该调用。

让我们看一下你的例子。我稍微修改了它以使用return foo(10),这样main编译器就不会完全优化所有内容!我还选择了 gcc__builtin_popcount作为 . 调用的未指定函数foo()。查看您的程序的这个 godbolt 版本,未经优化,在 gcc 6.2 中编译。装配体如下所示:

foo(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        popcnt  eax, eax
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, 10
        call    foo(int)
        pop     rbp
        ret
Run Code Online (Sandbox Code Playgroud)

这很简单。大部分foo()只是设置堆栈帧并(毫无意义地)将edivarA参数)推入堆栈。

当我们foo()从 main 调用时,我们10作为参数传递。很明显,它是一个常数这一事实并没有帮助。

好的,让我们用更实际的-O2设置1来编译它。这是我们得到的

main:
        mov     eax, 2
        ret
Run Code Online (Sandbox Code Playgroud)

就是这样。整个事情就这样return 2,差不多了。所以编译器肯定能够看到 10 是一个常量值,并展开foo(10)。此外,它能够foo(10)完全评估,直接计算 10(二进制为 0b1010)的popcountpopcount ,根本不需要指令,并且只返回答案2

另请注意,编译器甚至没有为foo()所有生成任何代码。这是因为它可以看到它被声明为static inline2,因此只能从该编译单元内调用它,并且实际上没有调用者需要完整的函数,因为唯一的调用站点是内联的。所以 foo 就消失了。

因此,标准中关于编译时常量的规定仅有助于理解编译器必须执行的操作以及某些表达式可以在何处合法使用,但对于理解编译器在优化实践中将执行的操作没有多大帮助。

这里的关键是你的方法foo()是在与其调用者相同的编译单元中声明的,因此编译器可以内联并有效地跨两个函数进行优化。如果它位于单独的编译单元中,则不会发生这种情况,除非您使用某些选项(例如链接时代码生成)。


1事实证明,这里几乎任何优化设置都会产生相同的代码,因为转换非常简单。

2事实上,或中的任何一个都足以使函数成为编译单元的本地函数。但是,如果省略两者,则会生成 for 的主体,因为可以从单独编译的单元调用它。经过优化,主体看起来像:inlinestaticfoo()

foo(int):
        xor     eax, eax
        popcnt  eax, edi
        ret
Run Code Online (Sandbox Code Playgroud)