将中间变量用于三元运算符(或类似运算符)以获得更好的性能
Using intermediate variables for ternary operator (or similar) for better performance?
假设在C++(或C、Java等)中,我有这样的代码:
int a = f() > g() ? f() : g();
它当然会给a赋值,返回值f()和g()之间的值越大。现在假设f()和g()本身是复杂而缓慢的,我应该用之类的东西替换这行吗
int f_value = f();
int g_value = g();
int a = f_value > g_value ? f_value : g_value;
所以f()和g()都不会被调用两次,或者编译器(给定足够的优化)无论如何都会为我做这样的事情,这样我就不必做任何事情了?
这个一般性的问题当然也适用于许多类似的场景。
一般来说,不,编译器不会执行此操作–实际上不能。调用f和g可能会产生副作用,第二次调用f或g的结果可能与第一次调用不同。想象一下这样的事情:
int f()
{
static int n = 0;
return ++n;
}
但也有例外证明了这一规则:
实际上,编译器可以执行它想要的任何优化–只要,优化的代码的行为与完全未优化的代码完全相同(考虑到任何可见的效果)。
因此,如果编译器能够保证省略第二个函数调用不会抑制任何副作用(而且只有这样!),那么它实际上可以优化第二个调用,而且很可能在更高的优化级别上也会这样做。
TL;DR:有称为min
和max
的函数。。。
编译器可能会也可能不会为您执行此优化。
从编译器的角度来看,f() > g() ? f() : g()
很可能是:
entry:
_0 = f();
_1 = g();
_cmp = _0 > _1
if _cmp: goto _greater; else: goto _lesser;
greater:
_2 = f();
goto end;
lesser:
_3 = g();
goto end;
end:
phi [greater _2], [lesser _3]
这被称为SSA表单(静态单一分配表单),并被大多数优化器(如LLVM和gcc)使用。
编译器是否对f()
或g()
求值一次或两次将取决于是否:
f()
和g()
要么注释为pure
,要么评估为pure
(无副作用,结果仅取决于输入)- 或者CCD_ 10和CCD_
- 或者
一般来说,我不会指望它。
然而,所有这些都无关紧要。
有更高级别的函数可以执行您想要的操作,例如此处的max
:
int a = std::max(f(), g());
在C++中,保证它只对f()
和g()
求值一次(求值顺序不保证,但两者都只求值一次,并且在调用max
之前)。
这严格等同于:
int _0 = f();
int _1 = g();
int a = std::max(_0, _1);
当然,要光滑得多。
"如果有足够的优化",编译器可能会执行此操作,这取决于函数f
和g
的特性。如果编译器能够看到函数的定义(因此它们位于调用它们的同一TU中,或者您正在使用链接时优化),并且能够看到它们没有副作用,并且它们的结果不依赖于任何全局变量,那么它只能对它们进行一次评估,而不是两次。
如果它们确实有副作用,那么你已经要求给它们打两次电话,所以其中一个会被评估两次。
如果他们是constexpr
,它可能不会给他们打电话。
对于您的示例,使用std::max(f(), g())
通常比使用中间变量更方便。与任何函数调用一样,它只对每个参数求值一次。
给定此代码:
int f(int x) {
return x + 1;
}
int g(int x) {
return x + 2;
}
int foo(int a, int b) {
return f(a) > g(b) ? f(a) : g(b);
}
我的机器上的gcc-O0生成以下内容。即使你读不懂,也要注意callq <_Z1fi>
会出现两次:
int foo(int a, int b) {
1e: 55 push %rbp
1f: 53 push %rbx
20: 48 83 ec 28 sub $0x28,%rsp
24: 48 8d ac 24 80 00 00 lea 0x80(%rsp),%rbp
2b: 00
2c: 89 4d c0 mov %ecx,-0x40(%rbp)
2f: 89 55 c8 mov %edx,-0x38(%rbp)
return f(a) > g(b) ? f(a) : g(b);
32: 8b 4d c0 mov -0x40(%rbp),%ecx
35: e8 c6 ff ff ff callq 0 <_Z1fi>
3a: 89 c3 mov %eax,%ebx
3c: 8b 45 c8 mov -0x38(%rbp),%eax
3f: 89 c1 mov %eax,%ecx
41: e8 c9 ff ff ff callq f <_Z1gi>
46: 39 c3 cmp %eax,%ebx
48: 7e 0a jle 54 <_Z3fooii+0x36>
4a: 8b 4d c0 mov -0x40(%rbp),%ecx
4d: e8 ae ff ff ff callq 0 <_Z1fi>
52: eb 0a jmp 5e <_Z3fooii+0x40>
54: 8b 45 c8 mov -0x38(%rbp),%eax
57: 89 c1 mov %eax,%ecx
59: e8 b1 ff ff ff callq f <_Z1gi>
}
5e: 48 83 c4 28 add $0x28,%rsp
62: 5b pop %rbx
63: 5d pop %rbp
64: c3 retq
而gcc-O2产生:
int foo(int a, int b) {
return f(a) > g(b) ? f(a) : g(b);
20: 8d 42 02 lea 0x2(%rdx),%eax
23: 83 c1 01 add $0x1,%ecx
26: 39 c1 cmp %eax,%ecx
28: 0f 4d c1 cmovge %ecx,%eax
}
2b: c3 retq
由于它可以看到f
和g
的定义,优化器已经有了自己的方法。
- C++:将控制台输出存储在宏中更好吗
- FFmpeg:制作一个应用程序比直接使用ffmepg更好吗
- 初始化具有非默认构造函数的std::数组项的更好方法
- 有没有比在库中添加一个并非由所有派生类实现的新虚拟函数更好的设计实践
- 为什么新的随机库比std::rand()更好
- 寻找一种更好的方法来表示无符号字符数组
- 哪种方法更好,性能明智
- 什么更好?返回对象指针列表?或返回指向对象列表的指针?
- 什么是更好的做法?通过指针或标识符传递类成员?
- 寻求更好地理解标准::访问
- 线程消息传递或更好:在"大师班"中访问其他班级的成员
- 有没有更好的方法来处理异常? try-catch块真的很丑
- 将中间变量用于三元运算符(或类似运算符)以获得更好的性能
- 哪个更好:重载运算符*还是重载强制转换运算符
- 在 C++98 中实现移动构造函数和移动赋值运算符以获得更好的性能
- 我将如何使用大小运算符delete/delete[],以及它们为什么更好
- 需要更好地理解循环后增量运算符
- 显式调用类的强制转换运算符方法是否比强制转换更好?
- 是否有更好的方法来重载类似的算术运算符
- 矩阵乘法哪个更好?GLM 的重载 * 运算符或直接使用着色器