Оптимизация переключателей — что они на самом деле делают?

Вероятно, все используют какие-то переключатели оптимизации (в случае с gcc наиболее распространенным является -O2 Я верю).

Но что делает gcc (и другие компиляторы, такие как VS, Clang) действительно делать при наличии таких вариантов?

Конечно, однозначного ответа нет, так как он очень сильно зависит от платформы, версии компилятора и т. Д.
Однако, если это возможно, я хотел бы собрать набор «правил большого пальца».
Когда мне следует подумать о некоторых хитростях для ускорения кода, а когда просто оставить работу компилятору?

Например, как далеко зайдет компилятор (немного искусственно …)
случаи, для разных уровней оптимизации:

1) sin(3.141592)
// будет ли он оцениваться во время компиляции, или я должен подумать о поисковой таблице, чтобы ускорить вычисления?

2) int a = 0; a = exp(18), cos(1.57), 2;
// будет ли компилятор вычислять exp и cos, хотя это и не нужно, так как значение выражения равно 2?

3)

for (size_t i = 0; i < 10; ++i) {
int a = 10 + i;
}

// компилятор пропустит весь цикл, поскольку у него нет видимых побочных эффектов?

Может быть, вы можете вспомнить другие примеры.

4

Решение

Если вы хотите знать, что делает компилятор, лучше всего взглянуть на документацию компилятора. Для оптимизации вы можете посмотреть на Анализ LLVM и проходы преобразования например.

1) sin (3.141592) // будет ли он оцениваться во время компиляции?

Наверное. Существует очень точная семантика для вычислений с плавающей запятой IEEE. Кстати, это может удивить, если вы поменяете флаги процессора во время выполнения.

2) int a = 0; a = exp (18), cos (1,57), 2;

Это зависит:

  • ли функции exp а также cos встроены или нет
  • если нет, правильно ли они аннотированы (чтобы компилятор знал, что у них нет побочных эффектов)

Для функций, взятых из вашей стандартной библиотеки C или C ++, они должны быть правильно распознаны / аннотированы.

Что касается исключения вычислений:

  • -adce: Устранение агрессивного мертвого кода
  • -dce: Устранение мертвого кода
  • -die: Устранение мертвых инструкций
  • -dse: Устранение мертвых магазинов

компиляторы любят находить бесполезный код 🙂

3)

Похожий на 2) на самом деле. Результат магазина не используется, а выражение не имеет побочных эффектов.

  • -loop-deletion: Удалить мертвые петли

И для финала: что не проверять компилятор?

#include <math.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
double d = sin(3.141592);
printf("%f", d);

int a = 0; a = (exp(18), cos(1.57), 2); /* need parentheses here */
printf("%d", a);

for (size_t i = 0; i < 10; ++i) {
int a = 10 + i;
}

return 0;
}

Clang пытается помочь уже во время компиляции:

12814_0.c:8:28: warning: expression result unused [-Wunused-value]
int a = 0; a = (exp(18), cos(1.57), 2);
^~~ ~~~~
12814_0.c:12:9: warning: unused variable 'a' [-Wunused-variable]
int a = 10 + i;
^

И излучаемый код (LLVM IR):

@.str = private unnamed_addr constant [3 x i8] c"%f\00", align 1
@.str1 = private unnamed_addr constant [3 x i8] c"%d\00", align 1

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind uwtable {
%1 = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([3 x i8]* @.str, i64 0, i64 0), double 0x3EA5EE4B2791A46F) nounwind
%2 = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([3 x i8]* @.str1, i64 0, i64 0), i32 2) nounwind
ret i32 0
}

Мы отмечаем, что:

  • как и предсказывал sin вычисление было решено во время компиляции
  • как и предсказывал exp а также cos были полностью раздеты.
  • как и предсказывалось, петля тоже была зачищена.

Если вы хотите углубиться в оптимизацию компилятора, я бы посоветовал вам:

  • научиться читать IR (это невероятно легко, правда, гораздо больше, чем эта сборка)
  • используйте страницу LLVM Try Out, чтобы проверить свои предположения
6

Другие решения

Компилятор имеет ряд этапов оптимизации. Каждый этап оптимизации отвечает за ряд небольших оптимизаций. Например, у вас может быть проход, который вычисляет арифметические выражения во время компиляции (например, вы можете выразить 5 МБ как 5 * (1024 * 1024) без штрафа). Еще один проход встроенных функций. Другой ищет недоступный код и убивает его. И так далее.

Затем разработчики компилятора решают, какой из этих проходов они хотят выполнить и в каком порядке. Например, предположим, у вас есть этот код:

int foo(int a, int b) {
return a + b;
}

void bar() {
if (foo(1, 2) > 5)
std::cout << "foo is large\n";
}

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

void bar() {
if (1 + 2 > 5)
std::cout << "foo is large\n";
}

Если вы запускаете сокращение выражений сейчас, сначала он решит, что 1 + 2 равно 3, а затем решит, что 3> 5 ложно. Итак, вы получите:

void bar() {
if (false)
std::cout << "foo is large\n";
}

А также сейчас удаление мертвого кода увидит if (false) и уничтожит его, поэтому результат:

void bar() {
}

Но теперь бар внезапно стал очень маленьким, когда раньше он был больше и сложнее. Так что, если вы снова запустите инлайнер, он сможет встроить бар в его вызывающих. Это может открыть еще больше возможностей для оптимизации и так далее.

Для разработчиков компиляторов это компромисс между временем компиляции и качеством сгенерированного кода. Они выбирают последовательность оптимизаторов для запуска, основываясь на эвристике, тестировании и опыте. Но так как один размер не подходит всем, они выставляют несколько ручек, чтобы настроить это. Основная ручка для gcc и clang — это опция -O. -O1 запускает короткий список оптимизаторов; -O3 запускает гораздо более длинный список, содержащий более дорогие оптимизаторы, и повторяет проходы чаще.

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

1

Компиляторы выполняют все виды оптимизации, о которых вы не можете думать. Особенно компиляторы C ++.

Они делают такие вещи, как развертывание циклов, встроенные функции, устранение мертвого кода, замена нескольких инструкций только одной и так далее.

Совет, который я могу дать: в компиляторах C / C ++ вы можете быть уверены, что они будут работать много оптимизаций.

Посмотрите на [1].

[1] http://en.wikipedia.org/wiki/Compiler_optimization

0
По вопросам рекламы [email protected]