Подтвердить что ты не робот

Использование промежуточных переменных для тройного оператора (или аналогичного) для лучшей производительности?

Предположим, что в С++ (или 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() не будут вызываться дважды, или компилятор (при достаточной оптимизации) будет делать что-то подобное для меня, так что мне не нужно ничего делать?

Этот общий вопрос, конечно, относится ко многим аналогичным сценариям.

4b9b3361

Ответ 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, оптимизатор имел с ними путь.