-O0
(неоптимизировано) по умолчанию .Он сообщает компилятору, что вы хотите, чтобы он компилировался быстро (короткое время компиляции), , а не , что требует дополнительного времени компиляции для создания эффективного кода.
(-O0
не означает буквально никакой оптимизации;например, gcc будет по-прежнему исключать код внутри блоков if(1 == 2){ }
. Особенно gcc больше, чем большинство других компиляторов, по-прежнему использует мультипликативные инверсии для деления при -O0
, потому что он все еще преобразует ваш источник C через множество внутренних представлений логики перед тем, как в конце концов выдатьasm.)
Плюс, «компилятор всегда прав» - это преувеличение даже при -O3
.Компиляторы очень хороши в больших масштабах, но незначительные пропущенные оптимизации все еще распространены в одиночных циклах.Часто с очень низким воздействием, но потраченные впустую инструкции (или мопы) в цикле могут занимать пространство в окне переупорядочения выполнения не по порядку и быть менее дружественными к гиперпоточности при совместном использовании ядра с другим потоком.См. C ++-код для проверки гипотезы Коллатца быстрее, чем рукописной сборки - почему? для получения дополнительной информации об избиении компилятора в простом конкретном случае.
Что более важно,-O0
также подразумевает обработку всех переменных, аналогичных volatile
, для согласованной отладки .то есть вы можете установить точку останова или один шаг и изменить значение переменной C, а затем продолжить выполнение и заставить программу работать так, как вы ожидаете, если ваш источник C работает на абстрактной машине C,Таким образом, компилятор не может выполнять какое-либо постоянное распространение или упрощение диапазона значений.(Например, целое число, которое, как известно, неотрицательно, может упростить вещи, используя его, или сделать некоторые, если условия всегда истинны или всегда ложны.)
(Это не совсем так плохо, как volatile
: множественные ссылки на одну и ту же переменную в одном выражении не всегда приводят к множественным нагрузкам; при -O0
компиляторы все равно будут несколько оптимизироваться в рамках одного выражения.)
Компиляторы должны специальнооптимизировать для -O0
, сохраняя / перезагружая все переменные по их адресу памяти между операторами .(В C и C ++ каждая переменная имеет адрес, если только она не была объявлена с помощью (теперь устаревшего) ключевого слова register
и никогда не получал свой адрес. Оптимизация адреса возможна в соответствии с правилом «как будто» для других переменных,но это не сделано в -O0
)
К сожалению, форматы отладочной информации не могут отслеживать местоположение переменной через регистры, поэтому полностью согласованная отладка невозможна без этого медленного и тупого кода-gen.
Если вам это не нужно, вы можете скомпилировать с -Og
для легкой оптимизации и без антиоптимизаций, необходимых для последовательной отладки.Руководство GCC рекомендует его для обычного цикла редактирования / компиляции / запуска, но вы будете «оптимизированы» для многих локальных переменных с автоматическим хранением при отладке.Глобальные и аргументы функций обычно обычно имеют свои фактические значения, по крайней мере на границах функций.
Еще хуже, -O0
делает код, который все еще работает, даже если вы используете команду GDB jump
дляпродолжить выполнение в другой строке источника .Таким образом, каждый оператор C должен быть скомпилирован в полностью независимый блок инструкций.( Можно ли «прыгать» / «пропускать» в отладчике GDB? )
for()
петли не могут быть преобразованы в идиоматические (для asm) do{}while()
циклы и другие ограничения.
По всем вышеуказанным причинам (микро-) бенчмаркинг неоптимизированного кода - огромная трата времени;результаты зависят от глупых деталей того, как вы написали исходный код, которые не имеют значения при компиляции с обычной оптимизацией.-O0
против -O3
производительность не связана линейно;некоторый код будет ускоряться намного больше, чем другие .
Узкие места в коде -O0
часто будут отличаться от -O3
- часто в счетчике циклов, который хранится в памяти, создавая цепочку зависимостей, переносимых циклами ~ 6 циклов. Это может создать интересные эффекты в asm, сгенерированном компилятором, например Добавление избыточного назначения ускоряет код при компиляции без оптимизации (что интересно с точки зрения asm, но не для C.)
«Мой тест оптимизирован иначе» - неоправданное оправдание для оценки производительности кода -O0
.
См. Справка по оптимизации цикла C для окончательного назначения для примера и более подробной информации о кроличьей норе, для которой настроена -O0
.
Получение интересного вывода компилятора
Если вы хотите увидеть, как компилятор добавляет 2 переменные, напишите функцию, которая принимает аргументы и возвращает значение . Помните, что вы хотите только смотреть на asm, но не запускать его, поэтому вам не нужны main
или любые числовые литеральные значения для всего, что должно быть переменной времени выполнения.
См. Также Как удалить «шум» из выходных данных сборки GCC / clang? Подробнее об этом.
float foo(float a, float b) {
float c=a+b;
return c;
}
компилируется с clang -O3
( в проводнике компилятора Godbolt ) до ожидаемого
addss xmm0, xmm1
ret
Но с -O0
это выливает аргументы в стек памяти. (Godbolt использует отладочную информацию, испускаемую компилятором, для цветового кодирования asm-инструкций в соответствии с тем, из какого оператора C они получены. Я добавил разрывы строк, чтобы показать блоки для каждого оператора, но вы можете увидеть это с помощью цветовой подсветки на ссылке Godbolt выше . Часто очень удобно для нахождения интересной части внутреннего цикла в оптимизированном выводе компилятора.)
gcc -fverbose-asm
будет помещать комментарии в каждой строке, показывая имена операндов как C vars. В оптимизированном коде это часто внутреннее имя tmp, но в неоптимизированном коде это обычно фактическая переменная из источника Си. Я вручную прокомментировал вывод clang, потому что он этого не делает.
# clang7.0 -O0 also on Godbolt
foo:
push rbp
mov rbp, rsp # make a traditional stack frame
movss DWORD PTR [rbp-20], xmm0 # spill the register args
movss DWORD PTR [rbp-24], xmm1 # into the red zone (below RSP)
movss xmm0, DWORD PTR [rbp-20] # a
addss xmm0, DWORD PTR [rbp-24] # +b
movss DWORD PTR [rbp-4], xmm0 # store c
movss xmm0, DWORD PTR [rbp-4] # return 0
pop rbp # epilogue
ret
Интересный факт: при использовании register float c = a+b;
возвращаемое значение может оставаться в XMM0 между операторами, а не разливаться / перезагружаться. Переменная не имеет адреса. (Я включил эту версию функции в ссылку Godbolt.)
Ключевое слово register
не влияет на оптимизированный код (за исключением того, что ошибочно берется адрес переменной, например, как const
в локальной системе не дает вам случайно что-то изменить). Я не рекомендую использовать его, но интересно видеть, что он действительно влияет на неоптимизированный код.
Связанный: