由于您的第二个问题更具体,我将首先解决它,然后结合第二个给出的上下文处理您的第一个问题。我想给出一个比这里已经存在的更基于证据的答案。
问题 #2:大多数编译器是否意识到变量已经被声明并且只是跳过该部分,或者它实际上是否每次都在内存中为它创建一个位置?
您可以通过在汇编程序运行之前停止编译器并查看 asm.xml 来自己回答这个问题。(-S
如果您的编译器具有 gcc 样式的接口,并且-masm=intel
如果您想要我在这里使用的语法样式,请使用该标志。)
在任何情况下,对于 x86-64 的现代编译器(gcc 10.2、clang 11.0),如果您禁用优化,它们只会在每次循环传递时重新加载变量。考虑以下 C++ 程序——为了直观地映射到 asm,我主要保留 C 风格并使用整数而不是字符串,尽管相同的原则适用于字符串情况:
#include <iostream>
static constexpr std::size_t LEN = 10;
void fill_arr(int a[LEN])
{
/* *** */
for (std::size_t i = 0; i < LEN; ++i) {
const int t = 8;
a[i] = t;
}
/* *** */
}
int main(void)
{
int a[LEN];
fill_arr(a);
for (std::size_t i = 0; i < LEN; ++i) {
std::cout << a[i] << " ";
}
std::cout << "\n";
return 0;
}
我们可以将其与具有以下差异的版本进行比较:
/* *** */
const int t = 8;
for (std::size_t i = 0; i < LEN; ++i) {
a[i] = t;
}
/* *** */
在禁用优化的情况下,gcc 10.2 在循环声明版本的每次循环中都将 8 放入堆栈:
mov QWORD PTR -8[rbp], 0
.L3:
cmp QWORD PTR -8[rbp], 9
ja .L4
mov DWORD PTR -12[rbp], 8 ;✷
而对于循环外版本它只执行一次:
mov DWORD PTR -12[rbp], 8 ;✷
mov QWORD PTR -8[rbp], 0
.L3:
cmp QWORD PTR -8[rbp], 9
ja .L4
这会对性能产生影响吗?在我将迭代次数推到数十亿之前,我没有看到它们与我的 CPU(Intel i7-7700K)在运行时方面的明显差异,即使这样,平均差异也小于 0.01 秒。毕竟,这只是循环中的一个额外操作。(对于一个字符串,循环内操作的差异显然要大一些,但不是很大。)
更重要的是,这个问题主要是学术问题,因为优化级别-O1
或更高的 gcc 会为两个源文件输出相同的 asm,clang 也是如此。因此,至少对于像这样的简单情况,无论哪种方式都不太可能对性能产生任何影响。当然,在现实世界的程序中,您应该始终分析而不是做出假设。
问题 #1:在循环中声明变量是好习惯还是坏习惯?
与几乎每个这样的问题一样,这取决于。如果声明在一个非常紧凑的循环内,并且您在没有优化的情况下进行编译,例如出于调试目的,那么理论上将其移出循环可能会提高性能,以便在您的调试工作中方便使用。如果是这样,它可能是明智的,至少在您进行调试时是这样。尽管我认为优化构建不会产生任何影响,但如果您确实观察到一个,您/您的配对/您的团队可以判断它是否值得。
同时,你不仅要考虑编译器是如何读取你的代码的,还要考虑它是如何传递给人类的,包括你自己。我想你会同意在尽可能小的范围内声明的变量更容易跟踪。如果它在循环之外,则意味着它需要在循环之外,如果事实并非如此,这会令人困惑。在大型代码库中,随着时间的推移,像这样的小混乱会随着时间的推移而增加,并且在工作数小时后变得令人疲倦,并可能导致愚蠢的错误。这可能比从轻微的性能改进中获得的成本要高得多,具体取决于用例。