编译时常量和形式参数

Compile-time const and formal parameters

在下面的例子中:

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

int main()
{
  foo(10);
  return 0;
}

这里的 varA 是编译时常量吗?

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

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

不,varA 不是编译时常量,它只是一个 const int 变量,这意味着它的值在函数 foo() 内不能更改。但是,编译器 可能 推断您使用编译时间常量 10 作为参数调用此函数,并编译此函数的一个版本,其中每次出现 varA 替换为 10.

不,varA 不是编译时常量 - 每次调用函数时肯定会有所不同。常量在标准中有明确的定义——一些关键的细节在this answer中有所涉及,或者你可以直接阅读标准中的官方词。

也就是说,您可能想知道编译器是否会 将其视为常量,在您使用常量值调用它的情况下,如您的示例中所示。对于任何启用优化的体面编译器,答案是 "yes"。调用内联和持续传播是实现这一目标的魔法。编译器将尝试内联对 foo 的调用,然后用 10 代替参数,并递归地执行。

让我们来看看你的例子。我稍微修改了它以在 main 中使用 return foo(10) 以便编译器不会完全优化所有内容!我还选择了 gcc 的 __builtin_popcount 作为 foo() 调用的未指定函数。检查 this godbolt version 未经优化的程序,在 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

很简单。大多数 foo() 只是设置堆栈帧并(毫无意义地)将 edivarA 参数)推到堆栈上。

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

好的,让我们用更现实的-O2设置1来编译它。 Here's what we get:

main:
        mov     eax, 2
        ret

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

另请注意,编译器甚至没有为 foo() 全部生成任何代码。那是因为它可以看到它被声明为 static inline2 所以它只能从这个编译单元中调用,而且实际上没有调用者需要完整的功能,因为只有呼叫站点被内联。所以 foo 就消失了。

因此,标准中关于编译时常量的内容仅有助于理解编译器必须做什么,以及某些表达式可以合法使用的地方,但它对理解编译器 在优化实践中做什么没有多大帮助。

这里的关键是您的方法 foo() 是在与其调用者相同的编译单元中声明的,因此编译器可以内联并有效地优化这两个函数。如果它在单独的编译单元中,则不会发生这种情况,除非您使用一些选项,例如 link-time code generation.


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

2事实上,或者或者inline或者static就足以让函数局部于编译单元。但是,如果两者都省略,则会生成 foo() 的主体,因为它可以从单独编译的单元中调用。经过优化,正文看起来像:

foo(int):
        xor     eax, eax
        popcnt  eax, edi
        ret