Gcc, похоже, упускает простую оптимизацию

Я пытаюсь ввести общую функцию с семантикой тернарного оператора: E1 ? E2 : E3. Я вижу, что компилятор может исключить вычисление одного из E2 или E3 в зависимости от условия E1 для тернарного оператора. Однако GCC пропускает эту оптимизацию в случае вызова функции ternary (даже если у E2 / E3 нет побочных эффектов).

В листинге ниже написано, что функция ternary ведет себя аналогично тернарному оператору. Однако GCC испускает потенциально тяжелый вызов функции f, которая, кажется, может быть устранена для некоторых входных значений (именно так, как это делается для тернарного оператора), потому что f объявлен с чистым атрибутом - пожалуйста, посмотрите ссылку Godbolt для кода сборки, созданного GCC.

Это что-то, что можно улучшить в GCC (место для оптимизации), или стандарт C++ явно запрещает такие оптимизации?

// Very heavy function
int f() __attribute__ ((pure));

inline int ternary(bool cond, int n1, int n2) {
    return cond ? n1 : n2;
}

int foo1(int i) {
    return i == 0 ? f() : 0;
}

int foo2(int i) {
    return ternary(i == 0, f(), 0);
}

Листинг сборки с -O3 -std=c++11:

foo1(int):
  test edi, edi
  jne .L2
  jmp f()
.L2:
  xor eax, eax
  ret
foo2(int):
  push rbx
  mov ebx, edi
  call f()
  test ebx, ebx
  mov edx, 0
  pop rbx
  cmovne eax, edx
  ret

https://godbolt.org/z/HfpNzo

Он "работает" с __attribute__ ((const)).

melpomene 13.09.2018 21:46

@melpomene: __attribute__ ((const)) означает, что функция не может даже читать глобальные объекты, в то время как __attribute__((pure)) означает, что она может читать глобальные файлы, но не имеет побочных эффектов. (gcc.gnu.org/onlinedocs/gcc/…). Поэтому я думаю, что это упущенная оптимизация, чтобы не оптимизировать вызов, потому что мы знаем, что у нее нет побочных эффектов. И это работает в случае foo1, но это потому, что абстрактная машина C не оценивает f() в неиспользуемой части троичного.

Peter Cordes 13.09.2018 21:49

@eXX: вам, вероятно, следует сообщить об этом как об ошибке keyword = missed-optimization в bugzilla GCC. gcc.gnu.org/bugzilla/enter_bug.cgi?product=gcc.

Peter Cordes 13.09.2018 21:50
"или стандарт C++ явно запрещает такую ​​оптимизацию?" Стандарт допускает любую оптимизацию при условии, что поведение результирующего исполняемого файла эквивалентно неоптимизированному. См. Правило как если бы.
François Andrieux 13.09.2018 21:51

Хех. Если я заменю foo1 на int t = f(); return i == 0 ? t : 0;, GCC не оптимизирует его (всегда вызывает f), но затем компилирует foo2 в jmp foo1.

melpomene 13.09.2018 21:53

Одна вещь, которая может сделать это сложным, заключается в том, что нет абсолютно никаких указаний на то, что вызов функции является дорогостоящим, а является указывает на то, что вызов функции нет является дорогостоящим: для этого требуется дополнительная ветвь.

user743382 13.09.2018 21:58

@hvd - хороший аргумент, но разве "более одной ветви" не являются разумной оценкой стоимости вызова функции?

463035818_is_not_a_number 13.09.2018 22:03

@ user463035818 Действительно, если бы разработчики GCC выбрали разумную оценку некоторые, то я бы ожидал, что эта оптимизация будет выполнена. Но я понятия не имел, как была бы выбрана разумная оценка.

user743382 13.09.2018 22:06

Интересно, что последний clang не оптимизирует его даже с атрибутом const: godbolt.org/z/58Vnt6

eXXXXXXXXXXX2 13.09.2018 22:12

При вызове foo2() язык требует, чтобы все параметры функции оценивались до ее вызова. ДАЖЕ, если функция встроена, компилятор вынужден оценивать все эти выражения как требование языка. Теперь компилятор может применить правило as-if постфактум и попытаться оптимизировать вызов f(), если нет побочных эффектов, но на самом деле это не такая простая оптимизация. Итак, вы сравниваете яблоки с апельсинами.

Martin York 13.09.2018 23:24

Что произойдет, если вы используете ленивую оценку параметров с помощью лямбда-выражений? `foo3 (я, [] () {return f ();}, [] () {return 0;});

Martin York 13.09.2018 23:28
6
11
425
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

I see that compiler is able to eliminate calculation of one of E2 or E3 depending on E1 condition (as long as E2/E3 has no side effects) for the ternary operator.

Компилятор не Устранить это; он просто никогда не оптимизирует его в cmov. Абстрактная машина C++ не оценивает неиспользуемую часть тернарного оператора.

int a, b;
void foo(int sel) {
    sel ? a++ : b++;
}

компилируется так (Godbolt):

foo(int):
    test    edi, edi
    je      .L2                # if(sel==0) goto
    add     DWORD PTR a[rip], 1   # ++a
    ret
.L2:
    add     DWORD PTR b[rip], 1   # ++b
    ret

Тернарный оператор может оптимизироваться до asm cmov только в том случае, если ни один из входных данных не имеет побочных эффектов. В остальном они не совсем эквивалентны.


В абстрактной машине C++ (т.е. на входе оптимизатора gcc) ваш foo2 всегда вызывает f(), а ваш foo1 - нет. Неудивительно, что foo1 компилируется именно так.

Чтобы foo2 скомпилировался таким образом, ему нужно было бы оптимизировать вызов f(). Он всегда вызывается для создания аргумента для ternary().


Здесь есть пропущенная оптимизация, о которой вы должны сообщить об ошибке GCC (используйте ключевое слово missed-optimization в качестве тега). https://gcc.gnu.org/bugzilla/enter_bug.cgi?product=gcc

Вызов int f() __attribute__ ((pure));должен можно оптимизировать. Он может глобально использовать читать, но не должен иметь никаких побочных эффектов. (https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html)

Как заметил @melpomene в комментариях, int f() __attribute__ ((const)); действительно дает вам оптимизацию, которую вы ищете. Функция __attribute__((const)) не может даже читать глобальные переменные, только свои аргументы. (Таким образом, без аргументов он всегда должен возвращать константу.)

HVD указывает, что у gcc нет информации о стоимости f(). Даже если он мог оптимизировал вызов ((pure)) f(), а также ((const)) f, возможно, он этого не сделал, потому что не знал, что это дороже, чем условное ветвление? Возможно, компиляция с оптимизацией на основе профиля убедит gcc что-то сделать?

Но учитывая, что он сделал вызов ((const)) f условным в foo2, gcc может просто не знать, что он может оптимизировать вызовы функций ((pure))? Может быть, он может только их CSE (если не были написаны глобальные переменные), но не оптимизировать полностью от базового блока? Или, может быть, текущий оптимизатор просто не может воспользоваться этим преимуществом. Как я уже сказал, похоже на ошибку пропущенного выбора.

Другие вопросы по теме