C/C++ 编译器优化:我应该更喜欢创建新变量、重用现有变量还是完全避免使用变量?
C/C++ compiler optimisations: should I prefer creating new variables, re-using existing ones, or avoiding variables altogether?
这是我一直想知道的事情:编译器是否更容易优化重新使用现有变量、创建新(理想情况下 const
)中间变量或创建变量的函数避免直接使用表达式?
例如,考虑以下函数:
// 1. Use expression as and when needed, no new variables
void MyFunction1(int a, int b)
{
SubFunction1(a + b);
SubFunction2(a + b);
SubFunction3(a + b);
}
// 2. Re-use existing function parameter variable to compute
// result once, and use result multiple times.
// (I've seen this approach most in old-school C code)
void MyFunction2(int a, int b)
{
a += b;
SubFunction1(a);
SubFunction2(a);
SubFunction3(a);
}
// 3. Use a new variable to compute result once,
// and use result multiple times.
void MyFunction3(int a, int b)
{
int sum = a + b;
SubFunction1(sum);
SubFunction2(sum);
SubFunction3(sum);
}
// 4. Use a new const variable to compute result once,
// and use result multiple times.
void MyFunction4(int a, int b)
{
const int sum = a + b;
SubFunction1(sum);
SubFunction2(sum);
SubFunction3(sum);
}
我的直觉是:
- 在这种特殊情况下,函数 4 最容易优化,因为它明确说明了使用数据的意图。它告诉编译器:“我们正在对两个输入参数求和,其结果不会被修改,并且我们以相同的方式将结果传递给每个后续函数调用。”我希望
sum
变量的值只会被放入一个寄存器中,不会发生实际的底层内存访问。
- 函数 1 是下一个最容易优化的函数,尽管它需要编译器方面的更多推理。编译器必须发现
a + b
以相同的方式用于每个函数调用,并且它必须知道每次使用该表达式时 a + b
的结果都是相同的。我仍然希望 a + b
的结果被放入寄存器而不是提交到内存中。但是,如果输入参数比普通 int
s 更复杂,我可以看到这更难以优化(临时规则将适用于 C++)。
- 函数 3 紧随其后:结果没有放入
const
变量,但编译器可以看到 sum
没有在函数的任何地方被修改(假设后续函数不对其进行可变引用),因此它可以像以前一样将值存储在寄存器中。不过,这比函数 4 的情况更不可能。
- 函数 4 对优化的帮助最少,因为它直接修改传入的函数参数。我不是 100% 确定编译器会在这里做什么:我认为期望它足够智能以发现
a
没有在函数的其他任何地方使用(类似于 sum
在功能 3) 中,但我不保证。这可能需要根据传入函数参数的方式修改堆栈内存(我不太熟悉函数调用在该细节级别的工作方式的来龙去脉)。
我的假设是否正确?是否还有更多因素需要考虑?
编辑:针对评论的一些澄清:
- 如果 C 和 C++ 编译器以不同的方式处理上述示例,我很想知道为什么。我可以理解,C++ 会根据对可能输入这些函数的对象的约束条件进行不同的优化,但是对于像
int
这样的原始类型,我希望它们使用相同的启发式方法。
- 是的,我可以进行优化编译并查看汇编输出,但我不懂汇编,因此我在这里提问。
优秀的现代编译器通常不会“关心”您用来存储值的名称。他们对这些值进行生命周期分析并基于此生成代码。例如,给定:
int x = complicated expression 0;
... code using x
x = complicated expression 1;
... code using x
编译器会看到在第一段代码中使用了complicated expression 0
,在第二段代码中使用了complicated expression 1
,名称x
是无关紧要的。结果将与代码使用不同的名称相同:
int x0 = complicated expression 0;
... code using x0
int x1 = complicated expression 1;
... code using x1
所以为了不同的目的重用一个变量是没有意义的;它不会帮助编译器节省内存或以其他方式优化。
即使代码在循环中,例如:
int x;
while (some condition)
{
x = complicated expression;
... code using x
}
编译器会看到complicated expression
诞生于循环体的开头,结束于循环体的结尾。
这意味着您不必担心编译器将如何处理代码。相反,您的决定应该主要以更清晰的编写和更有可能避免错误的内容为指导:
- 避免为多个目的重复使用一个变量。例如,如果有人稍后更新您的函数以添加新功能,他们可能会错过您已使用
a += b;
更改函数参数并在代码中稍后使用 a
的事实,就好像它仍然包含原始参数.
- 自由创建新变量来保存重复的表达式。
int sum = a + b;
可以;它表达了意图,并在多个地方使用相同的表达方式时使读者更清楚。
- 限制变量(和一般标识符)的范围。仅在需要它们的最内层范围内声明它们,例如在循环内部而不是外部。避免在不再适用的地方意外使用变量。
这是我一直想知道的事情:编译器是否更容易优化重新使用现有变量、创建新(理想情况下 const
)中间变量或创建变量的函数避免直接使用表达式?
例如,考虑以下函数:
// 1. Use expression as and when needed, no new variables
void MyFunction1(int a, int b)
{
SubFunction1(a + b);
SubFunction2(a + b);
SubFunction3(a + b);
}
// 2. Re-use existing function parameter variable to compute
// result once, and use result multiple times.
// (I've seen this approach most in old-school C code)
void MyFunction2(int a, int b)
{
a += b;
SubFunction1(a);
SubFunction2(a);
SubFunction3(a);
}
// 3. Use a new variable to compute result once,
// and use result multiple times.
void MyFunction3(int a, int b)
{
int sum = a + b;
SubFunction1(sum);
SubFunction2(sum);
SubFunction3(sum);
}
// 4. Use a new const variable to compute result once,
// and use result multiple times.
void MyFunction4(int a, int b)
{
const int sum = a + b;
SubFunction1(sum);
SubFunction2(sum);
SubFunction3(sum);
}
我的直觉是:
- 在这种特殊情况下,函数 4 最容易优化,因为它明确说明了使用数据的意图。它告诉编译器:“我们正在对两个输入参数求和,其结果不会被修改,并且我们以相同的方式将结果传递给每个后续函数调用。”我希望
sum
变量的值只会被放入一个寄存器中,不会发生实际的底层内存访问。 - 函数 1 是下一个最容易优化的函数,尽管它需要编译器方面的更多推理。编译器必须发现
a + b
以相同的方式用于每个函数调用,并且它必须知道每次使用该表达式时a + b
的结果都是相同的。我仍然希望a + b
的结果被放入寄存器而不是提交到内存中。但是,如果输入参数比普通int
s 更复杂,我可以看到这更难以优化(临时规则将适用于 C++)。 - 函数 3 紧随其后:结果没有放入
const
变量,但编译器可以看到sum
没有在函数的任何地方被修改(假设后续函数不对其进行可变引用),因此它可以像以前一样将值存储在寄存器中。不过,这比函数 4 的情况更不可能。 - 函数 4 对优化的帮助最少,因为它直接修改传入的函数参数。我不是 100% 确定编译器会在这里做什么:我认为期望它足够智能以发现
a
没有在函数的其他任何地方使用(类似于sum
在功能 3) 中,但我不保证。这可能需要根据传入函数参数的方式修改堆栈内存(我不太熟悉函数调用在该细节级别的工作方式的来龙去脉)。
我的假设是否正确?是否还有更多因素需要考虑?
编辑:针对评论的一些澄清:
- 如果 C 和 C++ 编译器以不同的方式处理上述示例,我很想知道为什么。我可以理解,C++ 会根据对可能输入这些函数的对象的约束条件进行不同的优化,但是对于像
int
这样的原始类型,我希望它们使用相同的启发式方法。 - 是的,我可以进行优化编译并查看汇编输出,但我不懂汇编,因此我在这里提问。
优秀的现代编译器通常不会“关心”您用来存储值的名称。他们对这些值进行生命周期分析并基于此生成代码。例如,给定:
int x = complicated expression 0;
... code using x
x = complicated expression 1;
... code using x
编译器会看到在第一段代码中使用了complicated expression 0
,在第二段代码中使用了complicated expression 1
,名称x
是无关紧要的。结果将与代码使用不同的名称相同:
int x0 = complicated expression 0;
... code using x0
int x1 = complicated expression 1;
... code using x1
所以为了不同的目的重用一个变量是没有意义的;它不会帮助编译器节省内存或以其他方式优化。
即使代码在循环中,例如:
int x;
while (some condition)
{
x = complicated expression;
... code using x
}
编译器会看到complicated expression
诞生于循环体的开头,结束于循环体的结尾。
这意味着您不必担心编译器将如何处理代码。相反,您的决定应该主要以更清晰的编写和更有可能避免错误的内容为指导:
- 避免为多个目的重复使用一个变量。例如,如果有人稍后更新您的函数以添加新功能,他们可能会错过您已使用
a += b;
更改函数参数并在代码中稍后使用a
的事实,就好像它仍然包含原始参数. - 自由创建新变量来保存重复的表达式。
int sum = a + b;
可以;它表达了意图,并在多个地方使用相同的表达方式时使读者更清楚。 - 限制变量(和一般标识符)的范围。仅在需要它们的最内层范围内声明它们,例如在循环内部而不是外部。避免在不再适用的地方意外使用变量。