1) Исследовать методы оптимизации кода на языке Си с помощью одного из компиляторов. Использовать для тестирования утилиту optbench.c
2) Исследовать особенности оптимизации компилятора на собственной программе.
компилятор программный код утилита
Постановка задачи
3) Выбрать один из предлагаемых компиляторов:
· Microsoft Visual Studio 2010
· GCC
· Intel C++ Compiler
1) Исследовать различные варианты оптимизаций для выбранного компилятора с помощью optbench.c, составить таблицу о проведенных улучшениях.
2) Привести примеры наиболее удачных и неудачных оптимизаций.
3) Подобрать опции компилятора для получения:
· наиболее быстрого кода
· наиболее компактного кода
Результаты выполненной работы
Компилятор
Для изучения оптимизации кода, написанного на языке С (optbench.c), был выбран компилятор GCC-4.4.3, работающий под операционной системой Linux Ubuntu v10.04.
Ассемблерный листинг
Чтобы получить ассемблерный код программы optbench.c, который находится в домашней директории, используем команду: gcc - S optbench. c
В результате в домашней папке был создан файл optbench. s, содержащий ассемблерный листинг неоптимизированной программы.
Для получения ассемблерного кода с оптимизациями использовалась команда: gcc - S - O 2 optbench. c
Исключено многократное помещение переменной i в регистр eax (исключены излишние операции загрузки). Вместо арифметических операций выполняется простое присваивание.
Деление на ноль
i2 = i / 0; flt_2 = flt_1 / 0.0;
-
-
Компилятор gcc версии 4.4.3 выдает ошибку деления на ноль и не генерирует объектный код.
Лишнее присваивание
k3 = 1; k3 = 1;
movl $1, k3 movl $1, k3
movl $1, k3
Исключается повторное присваивание значения переменной k3.
Снижение мощности
k2 = 4 * j5; for(i = 0; i <= 5; i++) ivector4[ i ] = i * 2;
movl j5, %eax sall $2, %eax movl %eax, k2 movl $0, i jmp.L4.L5: movl i, %eax movl i, %edx addl %edx, %edx movw %dx, ivector4(%eax,%eax) movl i, %eax addl $1, %eax movl %eax, i.L4: movl i, %eax cmpl $5, %eax jle.L5
Операция умножения заменяется более простым и быстрым сдвигом влево (sall). Шаг цикла увеличен в 2 раза, число переходов после оптимизации уменьшилось. Это должно уменьшить время работы.
Переменные i5 = 0, i2 = 5, i = 100, k5=5 уже имеют значения, поэтому часть кода после условного оператора if недостижима и игнорируется компилятором. Выполняться будет только код после оператора else. Поэтому необходимо ввести новые переменные ni, ni5, ni2, которыми заменим исходные. Значение суммы ni5+ni2 высчитывается два раза. Оптимизация глубоких подвыражений не производится.
Проверка того, как компилятор генерирует адрес переменной с константным индексом, размножает копии и регистры
Компилятор выполняет суммирование h3+k3 всего один раз перед условным оператором и использует полученное значение в дальнейшем как уже известное. Т.е. происходит удаление общих подвыражений.
Вынесение инвариантного кода (j * k) может быть вынесено из цикла
Так как конечные значения k5 и i5 можно посчитать без использования циклов, компилятор сразу присваивает переменной i значение равное 5 и подставляет его в формулы для k5 и i5. Следовательно, оптимизация объединения циклов не происходит из-за отсутствия циклов. Сохраним циклы путем добавления функций вывода значений k5 и i5 при каждой итерации. Оптимизация объединения циклов не выполнена компилятором.
Развертка циклов
void loop_unrolling(x) int x; { for(i = 0; i < 6; i++) ivector4[ i ] = 0; }
pushl %ebp movl %esp, %ebp movl $0, i jmp.L30.L31: movl i, %eax movw $0, ivector4(%eax,%eax) movl i, %eax addl $1, %eax movl %eax, i.L30: movl i, %eax cmpl $5, %eax jle.L31 popl %ebp
Развертка циклов выполняется. Цикл заменяется присваиваниями. Получаем линейный участок кода, что уменьшает время выполнения программы.
Сжатие цепочки переходов
int jump_compression(i, j, k, l, m) int i, j, k, l, m; { beg_1: if(i < j) if(j < k) if(k < l) if(l < m) l += m; else goto end_1; else k += l; else { j += k; end_1: goto beg_1; } else i += j; return(i + j + k + l + m); }
Происходит сжатие цепочки переходов. Компилятор уменьшает колличество переходов (количество переходов в оптимизированном коде на 2 меньше, чем в коде до оптимизации).
Таблица 2. Оптимизации, отсутствующие в файле optbench.c
Исходный текст на Си
Неоптимизированный код на ассемблере
Оптимизированный код на ассемблере
Результаты
Оптимизация переходов
int my_1(c) { if(a==b) c=1; if (a!=b) c=2; return c; }
В отличие от неоптимизированного кода, во втором случае второй условный оператор отсутствует. Вместо этого используется один сложный условный оператор.
Исключение общих операций из цикла.
int my_2(int *myvector, int d3) { for (w=0; w<10; w++) d3=d3+myvector[w]+S; return (d3); }
Компилятор избавляется от недостижимого кода в обоих случаях.
Примеры наиболее удачных и неудачных оптимизаций
1) Наиболее неудачной оптимизацией оказалось «Объединение циклов». Оптимизация не была выполнена, однако код стал неудобочитаемым.
) Наиболее удачной является оптимизация «Развертка циклов». Она заменила код с двумя переходами на линейный участок, что позволило уменьшить время выполнения программы. Также достаточно удачной оптимизацией является «Удаление недостижимого кода». Так как код, являющийся недостижимым, может быть достаточно большим. Удаляя данный код, мы уменьшаем объем исполняемого файла.
Таблица 3. Выполняемые оптимизации
Оптимизация
Выполнение
Размножение констант и копий
+
Свёртка констант, арифметические тождества и излишние операции загрузки/сохранения
+
Исключение деления на ноль
-
Лишнее присваивание
+
Снижение мощности
+
Простой цикл
+
Управление переменной индукции цикла
+
Глубокие подвыражения
-
Генерация адреса переменной с константным индексом, размножение копий и регистров
+
Удаление общих подвыражений
+
Вынесение инвариантного кода
+
Недостижимый код
+
Ненужный цикл
+
Слияние циклов
-
Развертка циклов
+
Сжатие цепочки переходов
+
Подбор опций компилятора
Таблица 4. Уровни оптимизации
Уровень оптимизации
Описание
-O0
Отключает оптимизацию. Только переменные, обьявленные register, сохраняются в регистрах.
-O(-O1)
Включает оптимизацию. Пытается уменьшить размер кода и ускорить работу программы. Соответственно увеличивается время компиляции. При указании -O активируются следующие флаги: -fauto-inc-dec -fcompare-elim -fcprop-registers -fdce -fdefer-pop -fdelayed-branch -fdse -fguess-branch-probability -fif-conversion2 -fif-conversion -fipa-pure-const -fipa-profile -fipa-reference -fmerge-constants -fsplit-wide-types -ftree-bit-ccp -ftree-builtin-call-dce -ftree-ccp -ftree-ch -ftree-copyrename -ftree-dce -ftree-dominator-opts -ftree-dse -ftree-forwprop -ftree-fre -ftree-phiprop -ftree-slsr -ftree-sra -ftree-pta -ftree-ter -funit-at-a-time
-O2
Оптимизирует еще больше. GCC выполняет почти все поддерживаемые оптимизации, которые не включают уменьшение времени исполнения за счет увеличения длины кода. Компилятор не выполняет раскрутку циклов или подстановку функций, когда вы указываете -O2. По сравнению с -O, эта опция увеличивает как время компиляции, так и эффективность сгенерированного кода. -O2 включает все флаги оптимизации наследованные от -O. Также включает следующие флаги оптимизации: -fthread-jumps -falign-functions -falign-jumps -falign-loops -falign-labels -fcaller-saves -fcrossjumping -fcse-follow-jumps -fcse-skip-blocks -fdelete-null-pointer-checks -fdevirtualize -fexpensive-optimizations -fgcse -fgcse-lm -fhoist-adjacent-loads -finline-small-functions -findirect-inlining -fipa-sra -foptimize-sibling-calls -fpartial-inlining -fpeephole2 -fregmove -freorder-blocks -freorder-functions -frerun-cse-after-loop -fsched-interblock -fsched-spec -fschedule-insns -fschedule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-switch-conversion -ftree-tail-merge -ftree-pre -ftree-vrp
-O3
Оптимизирует еще немного. Включает все оптимизации -O2 и также включает флаги: -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -ftree-vectorize -fvect-cost-model -ftree-partial-pre -fipa-cp-clone
-Os
Включает оптимизацию по размеру. -Os флаг активирует все флаги оптимизации из -O2, в основном те, которые не увеличивают размер выходного файла. В дальнейшем выполняются оптимизации по уменьшению размера кода. -Os выключает следующие флаги оптимизации: -falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays -ftree-vect-loop-version
Рассмотрим некоторые флаги оптимизации и их комбинации для конкретной программы. В качестве программы используем линейный конгруэнтный генератор случайных чисел (файл lkg.c). А в качестве применяемых оптимизаций возьмем флаги, представленные в таблице ниже (Таблица 5. Рассматриваемые флаги оптимизации).
Таблица 5. Рассматриваемые флаги оптимизации
Оптимизация (флаги)
Описание
-fomit-frame-pointer
Опция, которая говорит, что для доступа к переменным нужно использовать стек. С этой опцией практически невозможна отладка.
-funroll-loops
Выполняется оптимизация развертыванием циклов. Осуществляется для циклов, число итераций которых может быть определено во время компиляции или во время выполнения.
-funroll-all-loops
Выполняется оптимизация развертыванием циклов. Развертывает все циклы - обычно программы, скомпилированные с этой опцией, медленнее запускаются.
-ffast-math
Эта опция разрешает GCC нарушать некоторые ANSI или IEEE правила и/или спецификации в интересах оптимизации кода по скорости выполнения. Например, это позволяет компилятору предполагать, что параметры к функции sqrt - неотрицательные числа.
-fno-signed-zeros
Разрешить оптимизации для арифметики с плавающей точкой, которые игнорируют знаковость нуля.
Данные флаги будем применять для рассматриваемой программы. Будем находить время исполнения программы для каждого флага оптимизации или комбинации флагов. Также для каждой оптимизации найдем размер кода.