编译时常量和形式参数
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()
只是设置堆栈帧并(毫无意义地)将 edi
(varA
参数)推到堆栈上。
当我们从 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 inline
2 所以它只能从这个编译单元中调用,而且实际上没有调用者需要完整的功能,因为只有呼叫站点被内联。所以 foo 就消失了。
因此,标准中关于编译时常量的内容仅有助于理解编译器必须做什么,以及某些表达式可以合法使用的地方,但它对理解编译器 将 在优化实践中做什么没有多大帮助。
这里的关键是您的方法 foo()
是在与其调用者相同的编译单元中声明的,因此编译器可以内联并有效地优化这两个函数。如果它在单独的编译单元中,则不会发生这种情况,除非您使用一些选项,例如 link-time code generation.
1事实证明,这里的几乎 any 优化设置都会产生相同的代码,因为转换非常简单。
2事实上,或者或者inline
或者static
就足以让函数局部于编译单元。但是,如果两者都省略,则会生成 foo()
的主体,因为它可以从单独编译的单元中调用。经过优化,正文看起来像:
foo(int):
xor eax, eax
popcnt eax, edi
ret
在下面的例子中:
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()
只是设置堆栈帧并(毫无意义地)将 edi
(varA
参数)推到堆栈上。
当我们从 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 inline
2 所以它只能从这个编译单元中调用,而且实际上没有调用者需要完整的功能,因为只有呼叫站点被内联。所以 foo 就消失了。
因此,标准中关于编译时常量的内容仅有助于理解编译器必须做什么,以及某些表达式可以合法使用的地方,但它对理解编译器 将 在优化实践中做什么没有多大帮助。
这里的关键是您的方法 foo()
是在与其调用者相同的编译单元中声明的,因此编译器可以内联并有效地优化这两个函数。如果它在单独的编译单元中,则不会发生这种情况,除非您使用一些选项,例如 link-time code generation.
1事实证明,这里的几乎 any 优化设置都会产生相同的代码,因为转换非常简单。
2事实上,或者或者inline
或者static
就足以让函数局部于编译单元。但是,如果两者都省略,则会生成 foo()
的主体,因为它可以从单独编译的单元中调用。经过优化,正文看起来像:
foo(int):
xor eax, eax
popcnt eax, edi
ret