Почему компиляторы дублируют некоторые инструкции?

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

int gcd(unsigned x, unsigned y) {
  return x == 0 ? y : gcd(y % x, x);
}

Вот код сборки (сгенерированный clang 5.0 с включенными оптимизациями):

gcd(unsigned int, unsigned int): # @gcd(unsigned int, unsigned int)
  mov eax, esi
  mov edx, edi
  test edx, edx
  je .LBB0_1
.LBB0_2: # =>This Inner Loop Header: Depth=1
  mov ecx, edx
  xor edx, edx
  div ecx
  test edx, edx
  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx
  ret
.LBB0_1:
  ret

В следующем фрагменте:

  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx

Если прыжок не произойдет, eax переназначается без видимой причины.

Другим примером является два ret в конце функции: отлично работать.

Является ли компилятор просто недостаточно интеллектуальным или есть причина не удалять дубликаты?

Ответ 1

Компиляторы могут выполнять оптимизацию, которая не очевидна для людей, и удаление инструкций не всегда ускоряет работу.

Небольшой объем поиска показывает, что разные процессоры AMD имеют проблемы с прогнозированием ветвлений, когда RET сразу после условной ветки. Заполняя этот слот тем, что по существу не работает, проблема с производительностью устраняется.

Обновить:

Пример ссылки, раздел 6.2 "Руководства по оптимизации программного обеспечения для процессоров AMD64" (см. Http://support.amd.com/TechDocs/25112.PDF) гласит:

В частности, избегайте следующих двух ситуаций:

  • Любая ветвь (условная или безусловная), которая имеет однобайтную инструкцию RET с обратным возвратом в качестве своей цели. См. "Примеры".

  • Условная ветвь, которая встречается в коде непосредственно перед однобайтовой инструкцией RET с обратным возвратом.

Он также подробно рассказывает о том, почему цели перехода должны иметь выравнивание, которое также может объяснить дублирование RET в конце функции.

Ответ 2

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

  • не влияют на состояние регистров и памяти вообще, или
  • не влияют на состояние, которое имеет значение для пост-условий функции, не будут иметь значения для его публичного API.

Звучит как возможность для символических методов исполнения. Если решатель ограничений не находит точек ветвления для данного MOV, возможно, это действительно NOP.