将中间变量用于三元运算符(或类似运算符)以获得更好的性能

Using intermediate variables for ternary operator (or similar) for better performance?

本文关键字:运算符 更好 性能 变量 中间 三元 用于      更新时间:2023-10-16

假设在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:有称为minmax的函数。。。


编译器可能会也可能不会为您执行此优化。

从编译器的角度来看,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);

当然,要光滑得多。

"如果有足够的优化",编译器可能会执行此操作,这取决于函数fg的特性。如果编译器能够看到函数的定义(因此它们位于调用它们的同一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

由于它可以看到fg的定义,优化器已经有了自己的方法。