Глобальные и локальные оптимизации
Презентацию к лекции Вы можете скачать здесь.
Межпроцедурный анализ
Как совместить хороший стиль программирования и требования к быстродействию приложения?
- Модульность.
- Читаемость кода и использование подпрограмм для повторяющихся вычислений.
- Принцип реализации утилит как "черного ящика".
Модульность исходного кода усложняет задачу по его оптимизации.
Обсуждаемые в предыдущих разделах оптимизации процедурного уровня:
- эффективно работают только с локальными переменными
- всякий вызов функции - "черный ящик"
- неизвестны многие свойства переданных в процедуру параметров
- неизвестны свойства глобальных переменных
Для решения этих проблем необходимо исследование программы в целом.
Как совместить хороший стиль программирования и требования к быстродействию приложения?
Модульность – одно из базовых требований к большому программному продукту, необходимое как для грамотного управления проектом, так и для простоты поддержки и модификации программы.
Модульность существенно снижает возможности для оптимизации программы и получению оптимального кода.
Читаемость и использование утилит для повторяющихся вычислений также необходимые условия разработки сложных приложений. Одной из рекомендаций для программистов является рекомендация избегать больших функций.
Эти требования к качеству кода сильно усложняют работу по его оптимизации. Дело в том, что большинство алгоритмов оптимизации работают в рамках конкретной процедуры. Например, обсуждаемые в первых лекциях скалярные оптимизации.
В общем случае скалярные оптимизации – оптимизации процедурного уровня, эффективно работают только с локальными переменными, поскольку всякий вызов функции для них "черный ящик", способный произвести любые действия. (Изменить любые глобальные переменные, а также локальные переменные, адреса которых доступны функции через аргументы.)
На "Простейшие оптимизации программ" мы рассматривали основную формулу, составляющую основной смысл анализа потока данных (Data Flow Analisys)
В случае вызова неизвестной функции в базовом блоке p, необходимо все глобалы и локалы, доступные функции, поместить в killed(p).
Для качественных скалярных оптимизаций необходимы знания об свойствах функций, вызываемых внутри процедуры.
Некоторые основные проблемы оптимизаций процедурного уровня
- Скалярные оптимизации:
Для качественных скалярных оптимизаций необходимы знания о свойствах функций, вызываемых внутри процедуры.
- Оптимизации циклических конструкций:
Для таких оптимизаций необходимо
- корректное определение объектов, которые могут ссылаться на одну память
- знание свойств функций внутри циклов (не изменяют итерационные переменные, не содержат выхода из программы и т.д.)
- оценка количества итераций цикла
-
Векторизация:
необходима информация о выравнивании объектов в памяти
Рассмотрим некоторые факторы, которые негативно сказываются на эффективности оптимизаций процедурного уровня.
Скалярные оптимизации (Data Flow Analysis)
В случае вызова неизвестной функции в базовом блоке p, необходимо все глобалы и локалы, доступные функции, поместить в killed(p).
Для качественных скалярных оптимизаций необходимы знания об свойствах функций, вызываемых внутри процедуры.
Для цикловых оптимизаций необходимы:
корректное определение объектов, которые могут ссылаться на одну память |
знание свойств функций внутри циклов (не изменяют итерационные переменные, не содержат выхода из программы и т.д.) |
оценка количества итераций цикла |
Векторизация: необходима информация о выравнивании объектов в памяти
Протяжка константы через неизвестную функцию
Рассмотрим простую программу:
test.c: extern void unknown(int *a); int main(){ int a,b,c; a=5; c=a; unknown(&a); if(a==5) printf("a==5\n"); b=a; printf("%d %d %d\n",a,b,c); return(1); }
Сохранится ли if утверждение в результирующем коде?
Давайте проиллюстрируем обсуждаемую проблему со скалярными оптимизациями на примере протяжки констант
Ассемблер полученный с помощью icl: icс –O2 test.c –S
Вывод: В общем случае, когда о вызываемой функции ничего не известно, константа присвоенная переменной не протягивается через функцию, которая может изменить значение этой переменной.
CSE (Удаление общих подвыражений)
#include <math.h> #include <stdio.h> extern void unknown(); float a,b; int main() { float c,d; scanf_s("%f",&a); scanf_s("%f",&b); c=0; if(sqrt(a+b)>3) c=a+b; else unknown(); d=sqrt(a+b)+c; printf("d=%f\n",d); return 1; }
Здесь есть общее подвыражение sqrt(a+b). Будет ли CSE работать с этим подвыражением?
Другая популярная скалярная оптимизация – удаление общих подвыражений.
Однопроходная и двухпроходная компиляция
Для того, чтобы собрать информацию о свойствах функций необходим дополнительный проход. Но поскольку, каждая функция в свою очередь может вызывать другие функции, а также себя (рекурсия), то необходимо произвести анализ графа вызовов (Call graph).
Граф вызовов представляет взаимоотношение вызовов между процедурами в программе. Каждая вершина представляет процедуру и каждая грань (f,g) указывает, что процедура f вызывает процедуру g.
Граф может быть статическим, вычисленным на этапе компиляции или динамическим, т.е. отражающим реальные вызовы при выполнении программы. (VTune)
Одной из основных задач межпроцедурного анализа является построения графа вызовов и выяснения свойств функций на основе его анализа. (Например, глобальный анализ потоков данных требует знания о том, какие данные каждая функция модифицирует).
Граф вызовов может быть полным и неполным. Если при сборе проекта используются библиотеки, свойства функций которых мы не знаем, то граф будет являться неполным и полноценный анализ не будет осуществлен.
Место межпроцедурного анализа и межпроцедурных оптимизаций в архитектуре компилятора.
Два вида межпроцедурных оптимизаций:
- модульная
- оптимизация для всей программы
Qip[-] enable(DEFAULT)/disable single-file IP optimization within files /Qipo[n] enable multi-file IP optimization between files /Qipo-c generate a multi-file object file (ipo_out.obj) /Qipo-S generate a multi-file assembly file (ipo_out.asm) /Qipo-jobs<n> specify the number of jobs to be executed simultaneously during the IPO link phase
Вы видите блок IP/IPO оптимизации. Существуют два вида межпроцедурных оптимизаций – модульная и оптимизация для всей программы. В первом случае происходит анализ всех процедур для всей программы и строится полный граф вызовов. Построение и анализ графа может быть неприемлимым с точки зрения ресурсов (большое время для больших проектов), но такой анализ позволяет решать задачу анализа потока данных для глобальных переменных. Во втором случае анализ производится для процедур из одного модуля и строится неполный граф вызовов. Это позволяет эффективно решать задачу анализа потока данных для статических переменных за относительно малое время выполнения.
Некоторые ключи компилятора.
Изменится ли что-либо, если мы определим некую процедуру unknown и перекомпилируем с –ipo:
#include <stdio.h> extern float a,b; void unknown() { printf("a=%f b=%f\n",a,b); }
icl –Qipo test.c unknown.c –Ob0 –Qipo-S
Работают ли протяжка констант и удаление общих подвыражений?
Теперь можно повторить приведенные выше примеры неудовлетворительной работы скалярных оптимизаций и посмотреть будут ли они работать при включенном межпроцедурном анализе.
В приведенных выше примерах IPO позволяет улучшить качество последующих скалярных оптимизаций поскольку с помощью анализа графа вызовов и операторов каждой процедуры получает информацию о том какие глобальные переменные и аргументы модифицируются каждой процедурой.