Почему компиляция с -O делает эту функцию больше? - PullRequest
0 голосов
/ 17 ноября 2018

Рассмотрим эту функцию:

long foo(long x) {
    return 5*x + 6;
}

Когда я компилирую его с x86-64 gcc 8.2 с -O3 (или -O2 или -O1), он компилируется в:

foo:
  leaq 6(%rdi,%rdi,4), %rax  # 5 bytes: 48 8d 44 bf 06
  ret                        # 1 byte:  c3

Когда я использую -Os вместо этого, он компилируется в:

foo:
  leaq (%rdi,%rdi,4), %rax   # 4 bytes: 48 8d 04 bf
  addq $6, %rax              # 4 bytes: 48 83 c0 06
  ret                        # 1 byte:  c3

Последний на 3 байта длиннее. Разве -Os не должен производить наименьший возможный код, даже если что-то большее будет более эффективным? Почему здесь происходит обратное?

Годболт: https://godbolt.org/z/jzNquk

1 Ответ

0 голосов
/ 18 ноября 2018

Хотя -Os («оптимизировать по размеру»), как ожидается, будет производить код более компактный по сравнению с кодом, созданным с опциями -O1, -O2 и -O3 («оптимизация по скорости»), на самом деле неттакая гарантия, как прокомментировал @Robert Harvey.

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

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

O3:

  1. tmp1 <- <code>x
  2. tmp2 <-<code>tmp1 << 2
  3. tmp3 <- <code>tmp2 + x
  4. retval <- <code>tmp3 + 6

Os:

  1. tmp <- <code>x * 5
  2. tmp2 <- <code>tmp + 6
  3. retval <- <code>tmp2

Пока все хорошо - -Os выигрывает.Но после этого, примерно через 15 фаз, выполняется фаза Combine , которая пытается объединить последовательность инструкций в одну инструкцию.Для кода -O3 Combine может очень хитро свернуть его до инструкции leaq в конечном выводе, но для -Os, Combine не работает какмного хорошего, и не в состоянии свернуть код дальше.С этого момента код не сильно меняется при дальнейшей оптимизации.

Чтобы ответить на точный вопрос - почему GCC делает это (сгенерируйте код, который он делает во время Expand с помощью -O3, и почему Объединение не делает лучше в -Os), нужно изучить код GCC и выяснить, какие параметры GCC являются важными, а также решения, принятые на предыдущих этапах оптимизации.

Но дело в том, что, хотя GCC и выполняется в этом примере, он может быть лучшим выбором для большинства других примеров.Это вопрос деликатных компромиссов - нелегкая работа для авторов компиляторов!

Это может не полностью ответить на вопрос, но, надеюсь, даст полезную информацию.Если вы заинтересованы в проверке выходных данных GCC на каждом этапе оптимизации, вы можете добавить флаг компиляции -da, который будет генерировать аннотированные дампы деревьев для каждого этапа, и флаг -dP, который добавляет аннотации к сгенерированным деревьям.вывод сборки вместе с -S.

...