Использование промежуточных переменных для тройного оператора (или аналогичного) для лучшей производительности?
Предположим, что в С++ (или 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() не будут вызываться дважды, или компилятор (при достаточной оптимизации) будет делать что-то подобное для меня, так что мне не нужно ничего делать?
Этот общий вопрос, конечно, относится ко многим аналогичным сценариям.
Ответы
Ответ 1
Как правило, нет, компилятор не будет этого делать - не может, на самом деле. Вызов f и g может иметь побочные эффекты, и результат второго вызова f или g может не совпадать с первым вызовом. Представьте себе что-то вроде этого:
int f()
{
static int n = 0;
return ++n;
}
Но есть исключения, подтверждающие правило:
Фактически, компилятору разрешается выполнять любую оптимизацию, которую он хочет - до тех пор, пока оптимизированный код ведет себя точно так же (с учетом любых видимых эффектов), как и полностью неоптимизированный.
Таким образом, если компилятор может гарантировать, что пропуск второго вызова функции не подавляет никаких побочных эффектов (и только тогда!), Он фактически может оптимизировать второй вызов и, скорее всего, сделает это также на более высоких уровнях оптимизации.
Ответ 2
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
(без побочных эффектов, результат зависит исключительно от входов)
- или
f()
и g()
встроены в сторону вызова
- или...
В общем, я не стал бы рассчитывать на это.
Однако все это не имеет значения.
Существуют функции более высокого уровня, которые вы можете сделать, например max
здесь:
int a = std::max(f(), g());
гарантирует в С++, что он будет когда-либо оценивать f()
и g()
один раз (порядок оценки не гарантируется, но оба будут оцениваться только один раз и перед вызовом max
).
Это строго эквивалентно:
int _0 = f();
int _1 = g();
int a = std::max(_0, _1);
но, конечно, много slicker.
Ответ 3
"При достаточной оптимизации" компилятор может это сделать, в зависимости от характеристик функций f
и g
. Если компилятор может видеть определения функций (так что либо они находятся в одном TU, из которого они вызваны, либо вы используете оптимизацию времени ссылки), и можете видеть, что у них нет побочных эффектов и их результатов, t зависит от любых глобалов, тогда он может оценивать их только один раз, а не дважды.
Если у них есть побочные эффекты, вы потребовали, чтобы их вызывали дважды, и поэтому один из них будет оцениваться дважды.
Если они 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
, оптимизатор имел с ними путь.