Презентацию к лекции Вы можете скачать здесь.
Межпроцедурный анализ
Как совместить хороший стиль программирования и требования к быстродействию приложения?
Модульность исходного кода усложняет задачу по его оптимизации.
Обсуждаемые в предыдущих разделах оптимизации процедурного уровня:
Для решения этих проблем необходимо исследование программы в целом.
Как совместить хороший стиль программирования и требования к быстродействию приложения?
Модульность – одно из базовых требований к большому программному продукту, необходимое как для грамотного управления проектом, так и для простоты поддержки и модификации программы.
Модульность существенно снижает возможности для
Читаемость и использование утилит для повторяющихся вычислений также необходимые условия разработки сложных приложений. Одной из рекомендаций для программистов является рекомендация избегать больших функций.
Эти требования к качеству кода сильно усложняют работу по его оптимизации. Дело в том, что большинство алгоритмов оптимизации работают в рамках конкретной процедуры. Например, обсуждаемые в первых лекциях скалярные оптимизации.
В общем случае скалярные оптимизации – оптимизации процедурного уровня, эффективно работают только с локальными переменными, поскольку всякий вызов функции для них "черный ящик", способный произвести любые действия. (Изменить любые глобальные переменные, а также локальные переменные, адреса которых доступны функции через аргументы.)
На лекции 3 мы рассматривали основную формулу, составляющую основной смысл
В случае вызова неизвестной функции в базовом блоке p, необходимо все глобалы и локалы, доступные функции, поместить в killed(p).
Для качественных скалярных оптимизаций необходимы знания об свойствах функций, вызываемых внутри процедуры.
Некоторые основные проблемы оптимизаций процедурного уровня
Для качественных скалярных оптимизаций необходимы знания о свойствах функций, вызываемых внутри процедуры.
Для таких оптимизаций необходимо
необходима информация о выравнивании объектов в памяти
Рассмотрим некоторые факторы, которые негативно сказываются на эффективности оптимизаций процедурного уровня.
Скалярные оптимизации (
В случае вызова неизвестной функции в базовом блоке 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). Будет ли
Другая популярная скалярная оптимизация – удаление общих
icl
Однопроходная и двухпроходная компиляция
Для того, чтобы собрать информацию о свойствах функций необходим дополнительный проход. Но поскольку, каждая функция в свою очередь может вызывать другие функции, а также себя (рекурсия), то необходимо произвести анализ
(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/
Некоторые ключи компилятора.
Изменится ли что-либо, если мы определим некую процедуру unknown и перекомпилируем с –
#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
Работают ли протяжка констант и удаление общих
Теперь можно повторить приведенные выше примеры неудовлетворительной работы скалярных оптимизаций и посмотреть будут ли они работать при включенном межпроцедурном анализе.
В приведенных выше примерах
Анализ совмещений (alias analysis)
Важная часть механизма определения зависимостей.
В случае с анализом совмещения указателей с каждым указателем связывается множество возможных значений. Два указателя могут ссылаться на одну область памяти, если пересечение множеств их допустимых значений не пусто.
Пример на LPT анализ
Можно ли осуществлять цикловые оптимизации с циклом for?
Межпроцедурный анализ используется для протяжки атрибутов функций. Например, есть атрибуты no_side_effect, always_return и т.д.
Продвижение данных (Data promotion). Каждая переменная имеет свою область видимости (scope).
Функции, используемые только в одной программе получают атрибут static.
Удаление неиспользуемых глобальных переменных.
Удаление мертвого кода.
В данном случае, функция не будет включаться в исполняемый модуль, если она не входит в inline). (Вывод – для улучшения размеров кода и времени компиляции используйте атрибут static).
Протяжка информации о выравнивании аргументов. Если актуальные аргументы функции всегда выровнены, то мы можем улучшить
Межпроцедурные оптимизации
Используются связи, возникающие при вызовах процедур, для того, чтобы оптимизировать одну или несколько процедур или выяснить, как они соотносятся друг с другом.
Протяжка константных аргументов
Если при каждом вызове процедуры f(i,j,k) в программе в качестве аргумента i всегда передается некая константа, то это позволяет присвоить первому формальному аргументу значение этой константы в коде процедуры f().
Протяжка возвращаемых значений
Если процедура всегда возвращает константное значение, то возможно протянуть это значение наружу.
То же самое касается передаваемых в процедуру аргументов. Если перед выходом из процедуры значение аргумента константа, то ее также можно протянуть наружу.
Пример. Константа протягивается в функцию через аргумент. Константа протягивается в вызывающую функцию.
Таким образом, протянув константу удалось избавиться от ветвления.
Чтобы не быть голословным можно рассмотреть простенький пример.
Подстановка (inlining)
Подстановка удаляет накладные расходы связанные с подготовкой к вызову функции, удаляет переходы, которые могут быть источниками неэффективной работы памяти, позволяет эффективнее применять скалярные и оптимизации циклов. Недостаток оптимизации – увеличение размера приложения. Как следствие – увеличение времени компиляции и необходимых для компиляции ресурсов.
Эвристики для подстановок пытаются выбрать наиболее выгодные для подстановки функции с тем, чтобы получить максимальный эффект для производительности, не выходя за пределы допустимого увеличения кода.
Атрибут функции inline
inline int exforsys(int x1) {
return 5*x1;
}
Программист "рекомендует" компилятору сделать подстановку такой функции.
Управление подстановкой
Синтаксис:
#pragma inline[recursive]
#pragma forceinline[recursive]
#pragma noinline
Аргумент Recursive
| требует чтобы данная директива применялась ко всем вызовам, которые осуществляются данным вызовом. |
Директива
inline рекомендует инлайнить |
noinline требует не инлайнить |
forceinline требует инлайнить |
Аналоги для языка Фортран
| cDEC$ ATTRIBUTES INLINE :: procedure |
| cDEC$ ATTRIBUTES NOINLINE :: procedure |
| cDEC$ ATTRIBUTES FORCEINLINE :: procedure |
Компиляторные опции подстановки
/Ob<n> control inline expansion:
n=0 disable inlining
n=1 inline functions declared with __inline, and perform C++ inlining
n=2 inline any function, at the compiler's discretion
/Qinline-min-size:<n>
set size limit for inlining small routines
/Qinline-min-size-
no size limit for inlining small routines
/Qinline-max-size:<n>
set size limit for inlining large routines
/Qinline-max-size-
no size limit for inlining large routines
/Qinline-max-total-size:<n>
maximum increase in size for inline function expansion
/Qinline-max-total-size-
no size limit for inline function expansion
Компилятор имеет массу опций, которые управляют инлайнингом, но как легко убедится в основном это опции регулирующие допустимое увеличение размера кода.
/Qinline-max-per-routine:<n>
maximum number of inline instances in any function
/Qinline-max-per-routine-
no maximum number of inline instances in any function
/Qinline-max-per-compile:<n>
maximum number of inline instances in the current compilation
/Qinline-max-per-compile-
no maximum number of inline instances in the current compilation
/Qinline-factor:<n>
set inlining upper limits by n percentage
/Qinline-factor-
do not set set inlining upper limits
/Qinline-forceinline
treat inline routines as forceinline
/Qinline-dllimport
allow(DEFAULT)/disallow functions declared __declspec(dllimport) to be inlined
/Qinline-calloc directs the compiler to inline calloc() calls as malloc()/memset()
Клонирование функций
Если в функцию f(x,y,x) передается x=2 в одном случае и x=3 в другом, то возможно заменить вызов функции f на вызовы f2 и f3.
Частичная подстановка
Если в функции f содержится вначале f.
Девиртуализация для C++
Вызов функций через указатели дороже простого вызова.
C++ - объектно-ориентированный язык поддерживающий высокий уровень абстракции.
Возможность выполнения методов функции в зависимости от типа объекта времени выполнения.
A => B => C
Все наследуемые классы переопределяют virtual int foo()
int process(class A *a) {
return(a->foo());
}
Еще один пример полезной IP оптимизации
Одной из проблем затрудняющей работу
Одной из межпроцедурных оптимизаций является девиртуализация функций. Функцию можно девиртуализовать, если в программе не используются объекты родительского класса.

Определение выгодности оптимизаций
невыгодно сохранять общее
z=x*y;
if(почти_никогда) {
t=x*y;
}
невыгодно выносить инвариант из цикла, если он может ни разу не использоваться при расчетах
for(i=0;i<n;i++) {
…
if(почти_никогда) {
t = invariant; break; }
}
| выгодно группировать вместе наиболее "часто" используемые фрагменты программы |
| невыгодно выполнять подстановку функции, которая "редко" вызывается в процессе выполнения программы. |
Сейчас я сформулирую несколько общих соображений, которые вроде бы очевидно вытекают из общих принципов улучшения производительности, которые мы рассматривали в предыдущих лекциях.
| выгодно комбинировать вместе "часто" совместно используемые элементы структуры |
| невыгодно тратить время на оптимизацию редко используемых фрагментов кода. |
Статистический профилировщик вычисляет вероятность условных переходов и вес базовых блоков. Это делается на основе анализа исходного кода. Межпроцедурный анализ пытается рассчитать вес процедур на основе анализа
Анализ исходного кода не может обеспечить точное вычисление весовых характеристик. В общем случае, не известны входные данные исполняемой программы, время вычисления ограничено.
Существует встроенная функция builtin_expect предназначенная для передачи компилятору информации о вероятности ветвления.
if(x) => if(__builtin_expect(x,1))
Т.е. При определении выгодности оптимизаций большую роль играет вероятность того или иного события а так-же "условный вес" того или иного базового блока. Эту информацию предоставляет статический профилировщик (static profiler). Его задача на основе анализа кода программы оценить вероятность того или иного перехода, частоту выполнения инструкции, частоту использования полей структуры и т.д. На основании оценок сделанных профилировщиком подставляются функции, оптимизируются переходы и делаются
В качестве примера можно рассмотреть вопрос определения вероятности того, что фигура на доске является пешкой. Статистический профилировщик вычисляет эту вероятность на основе анализа перечня всех имеющихся фигур.
Динамический профилировщик собирает весовые характеристики на основе анализа статистики собранной при запуске инструментированного приложения. Инструментация в данном случае – снабжение приложения механизмами для сбора статистики.
/Qprof-gen[:keyword]
instrument program for profiling.
Optional keyword may be srcpos or globdata
/Qprof-use[:<arg>]
enable use of profiling information during optimization
weighted - invokes profmerge with -weighted option to scale data
based on run durations
[no]merge - enable(default)/disable the invocation of the profmerge
tool
Динамический профилировщик работает иначе. Сначала подготавливается специальная программа с профилировкой, т.е. в нее вставляются различные счетчики. Затем программа запускается на обычном для нее наборе данных и собирает информацию о выполнении в специальный файл. Затем осуществляется перекомпиляция данной программы с учетом собранной статистики. Это позволяет лучше оптимизировать переходы, подставить функции, а также лучше распараллелить и завекторизовать циклы.
Последовательность действий при использовании динамического
/Qprof_gen/Qprof_use для использование собранных статистик при компиляции.Даже если вы используете очень разные данные, тем не менее динамический профилировщик в состоянии улучшить статистику для многих переходов внутри программы.
Информация собранная динамическим профилировщиком более точная, поэтому некоторые оптимизации, применение которых может дать большой негативный эффект при неправильном применении могут выполняться только при наличии информации от динамического
Некоторые оптимизации которые базируются на профилировочной информации:
| перестановки базовых блоков |
| группировка часто используемых функций |
| вынос "холодных" базовых блоков за |
Динамическое выделение памяти
Объекты и массивы могут инициализироваться динамически во время исполнения приложения с использованием операторов new и delete, malloc и free. Менеджер памяти, это часть приложения, обрабатывающая запросы на выделение и освобождение памяти.
Типичные ситуация, когда динамическое выделение памяти необходимо:
Неудобства
Менеджер памяти – это часть приложения, обрабатывающий запросы на выделение и освобождение памяти. Менеджер памяти или прилинковывается статически в момент линковки или может вызываться из динамической библиотеки. Одним из показателей качества работы менеджера памяти является быстрота предоставления необходимой памяти, а так же быстрота освобождения адресов и умение объединять неиспользованные блоки. В процессе длительной работы приложения качество используемой памяти может ухудшаться.
Помимо этого менеджер памяти может оказывать значительное влияние на производительность приложения.
Одной из значимых проблем для производительности современных программ, написанных на C и C++, которые работают с
Определяет влияние менеджера памяти на производительность компактность размещения в памяти совместно используемых объектов. В этом случае больше вероятность, что при обращении к объекту он окажется в кэш-памяти.
Важно оценить сильные и слабые стороны использования динамической памяти при проектировании приложения.
Различные менеджеры памяти реализуют различные алгоритмы выделения памяти и пытаются снизить затраты на работу с динамической памятью.
Альтернативные менеджеры памяти:
| Smartheap |
| dlmalloc |
Одним из важных факторов производительности в C++ является компактность размещения в памяти совместно используемых объектов, объединенных в различные связные списки. Связный список менее эффективен чем
В случае работы с массивами непрерывный массив также выгоднее чем массив указателей по причине лучшей работы системы памяти.
При выделении памяти для динамических объектов память выделяется случайным образом и вероятность того, что например элементы списка будут хорошо располагаться в памяти очень невелика. Чтобы решить эту проблему менеджеры памяти пытаются использовать различные методы, например сортируют выделяемые блоки по размеру и т.д.
Связные списки в памяти
| Связный список: | ![]() |
| Может располагаться в памяти так: | ![]() |
| И в физической памяти: | ![]() |
При работе со связными списками, в том случае если мы аллоцируем память для каждого последующего объекта с помощью динамического менеджера памяти, память может выделиться достаточно произвольным образом внутри виртуальной памяти. В случае работы с многопоточным приложением на многопроцессорной машине память для элементов списков может быть выделена на физической памяти разных процессоров.

Идея данного примера состоит в замене умолчательного выделения памяти для массива указателей на выделении памяти из одного предварительно аллоцированного большого блока памяти. В этом случае работа с массивом указателей будет идентична работе с непрерывным массивом.
Контейнеры
Существует популярный способ улучшения работы с динамически выделяемой памятью при помощи контейнеров.
Создание и использование контейнеров это один из примеров эффективного использования шаблонов (template) в С++. Наиболее распространенный набор контейнеров предоставляется Standard Template Library (STL), которая поставляется с современными C++ компиляторами.
Однако STL в основном разрабатывалась для гибкости использования и вопросы производительности имели более низкий приоритет. Поэтому выделение памяти для хранения объектов осуществляется пошагово по мере роста количества объектов, которые необходимо хранить в памяти. Отсутствует гибкая система, позволяющая заранее определять сколько памяти необходимо выделить изначально. В случае расширения контейнера может возникнуть необходимость копирования содержания контейнера. Это копирование происходит с использованием конструкторов копирования.
Еще один популярный метод – это метод пулов памяти. Одно из его отличий состоит в том, что при расширении пула весь блок памяти копируется с помощью memcpy.

Кодогенератор
Кодогенерация — часть процесса компиляции, когда специальная часть компилятора, кодогенератор, конвертирует синтаксически корректную программу в последовательность инструкций, которые могут выполнятся на машине. При этом могут применятся различные, в первую очередь машинно-зависимые оптимизации. Часто кодогенератор является общей частью для множества компиляторов, каждый из которых генерирует промежуточный код, подаваемый на вход кодогенератору.
Планирование инструкций (instruction scheduling)
Планирование инструкций это компьютерная оптимизация, используемая для улучшения уровня инструкционного параллелизма. Оптимизация обычно осуществляется за счет изменения порядка инструкций для уменьшения задержек процессорного конвейера.
Процессор имеет собственный механизм для планирования инструкций и распределения их по исполняющим устройствам. Этот механизм предусматривает упреждающий просмотр поступающих инструкций. Но он может быть недостаточно эффективен поскольку "окно упреждающего просмотра" ограничено. Инструкции могут переставляться в соответствии со следующими соображениями:
Планирование инструкций может производиться внутри одного базового блока или внутри суперблока, объединения нескольких базовых блоков. Некоторые инструкции могут передвигаться за границы соответствующих им базовых блоков.
Планирование инструкций может осуществляться как до, так и после распределения регистров.
Презентацию к лекции Вы можете скачать здесь.
Межпроцедурный анализ
Как совместить хороший стиль программирования и требования к быстродействию приложения?
Модульность исходного кода усложняет задачу по его оптимизации.
Обсуждаемые в предыдущих разделах оптимизации процедурного уровня:
Для решения этих проблем необходимо исследование программы в целом.
Как совместить хороший стиль программирования и требования к быстродействию приложения?
Модульность – одно из базовых требований к большому программному продукту, необходимое как для грамотного управления проектом, так и для простоты поддержки и модификации программы.
Модульность существенно снижает возможности для
Читаемость и использование утилит для повторяющихся вычислений также необходимые условия разработки сложных приложений. Одной из рекомендаций для программистов является рекомендация избегать больших функций.
Эти требования к качеству кода сильно усложняют работу по его оптимизации. Дело в том, что большинство алгоритмов оптимизации работают в рамках конкретной процедуры. Например, обсуждаемые в первых лекциях скалярные оптимизации.
В общем случае скалярные оптимизации – оптимизации процедурного уровня, эффективно работают только с локальными переменными, поскольку всякий вызов функции для них "черный ящик", способный произвести любые действия. (Изменить любые глобальные переменные, а также локальные переменные, адреса которых доступны функции через аргументы.)
На лекции 3 мы рассматривали основную формулу, составляющую основной смысл
В случае вызова неизвестной функции в базовом блоке p, необходимо все глобалы и локалы, доступные функции, поместить в killed(p).
Для качественных скалярных оптимизаций необходимы знания об свойствах функций, вызываемых внутри процедуры.
Некоторые основные проблемы оптимизаций процедурного уровня
Для качественных скалярных оптимизаций необходимы знания о свойствах функций, вызываемых внутри процедуры.
Для таких оптимизаций необходимо
необходима информация о выравнивании объектов в памяти
Рассмотрим некоторые факторы, которые негативно сказываются на эффективности оптимизаций процедурного уровня.
Скалярные оптимизации (
В случае вызова неизвестной функции в базовом блоке 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). Будет ли
Другая популярная скалярная оптимизация – удаление общих
icl
Однопроходная и двухпроходная компиляция
Для того, чтобы собрать информацию о свойствах функций необходим дополнительный проход. Но поскольку, каждая функция в свою очередь может вызывать другие функции, а также себя (рекурсия), то необходимо произвести анализ
(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/
Некоторые ключи компилятора.
Изменится ли что-либо, если мы определим некую процедуру unknown и перекомпилируем с –
#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
Работают ли протяжка констант и удаление общих
Теперь можно повторить приведенные выше примеры неудовлетворительной работы скалярных оптимизаций и посмотреть будут ли они работать при включенном межпроцедурном анализе.
В приведенных выше примерах
Анализ совмещений (alias analysis)
Важная часть механизма определения зависимостей.
В случае с анализом совмещения указателей с каждым указателем связывается множество возможных значений. Два указателя могут ссылаться на одну область памяти, если пересечение множеств их допустимых значений не пусто.
Пример на LPT анализ
Можно ли осуществлять цикловые оптимизации с циклом for?
Межпроцедурный анализ используется для протяжки атрибутов функций. Например, есть атрибуты no_side_effect, always_return и т.д.
Продвижение данных (Data promotion). Каждая переменная имеет свою область видимости (scope).
Функции, используемые только в одной программе получают атрибут static.
Удаление неиспользуемых глобальных переменных.
Удаление мертвого кода.
В данном случае, функция не будет включаться в исполняемый модуль, если она не входит в inline). (Вывод – для улучшения размеров кода и времени компиляции используйте атрибут static).
Протяжка информации о выравнивании аргументов. Если актуальные аргументы функции всегда выровнены, то мы можем улучшить
Межпроцедурные оптимизации
Используются связи, возникающие при вызовах процедур, для того, чтобы оптимизировать одну или несколько процедур или выяснить, как они соотносятся друг с другом.
Протяжка константных аргументов
Если при каждом вызове процедуры f(i,j,k) в программе в качестве аргумента i всегда передается некая константа, то это позволяет присвоить первому формальному аргументу значение этой константы в коде процедуры f().
Протяжка возвращаемых значений
Если процедура всегда возвращает константное значение, то возможно протянуть это значение наружу.
То же самое касается передаваемых в процедуру аргументов. Если перед выходом из процедуры значение аргумента константа, то ее также можно протянуть наружу.
Пример. Константа протягивается в функцию через аргумент. Константа протягивается в вызывающую функцию.
Таким образом, протянув константу удалось избавиться от ветвления.
Чтобы не быть голословным можно рассмотреть простенький пример.
Подстановка (inlining)
Подстановка удаляет накладные расходы связанные с подготовкой к вызову функции, удаляет переходы, которые могут быть источниками неэффективной работы памяти, позволяет эффективнее применять скалярные и оптимизации циклов. Недостаток оптимизации – увеличение размера приложения. Как следствие – увеличение времени компиляции и необходимых для компиляции ресурсов.
Эвристики для подстановок пытаются выбрать наиболее выгодные для подстановки функции с тем, чтобы получить максимальный эффект для производительности, не выходя за пределы допустимого увеличения кода.
Атрибут функции inline
inline int exforsys(int x1) {
return 5*x1;
}
Программист "рекомендует" компилятору сделать подстановку такой функции.
Управление подстановкой
Синтаксис:
#pragma inline[recursive]
#pragma forceinline[recursive]
#pragma noinline
Аргумент Recursive
| требует чтобы данная директива применялась ко всем вызовам, которые осуществляются данным вызовом. |
Директива
inline рекомендует инлайнить |
noinline требует не инлайнить |
forceinline требует инлайнить |
Аналоги для языка Фортран
| cDEC$ ATTRIBUTES INLINE :: procedure |
| cDEC$ ATTRIBUTES NOINLINE :: procedure |
| cDEC$ ATTRIBUTES FORCEINLINE :: procedure |
Компиляторные опции подстановки
/Ob<n> control inline expansion:
n=0 disable inlining
n=1 inline functions declared with __inline, and perform C++ inlining
n=2 inline any function, at the compiler's discretion
/Qinline-min-size:<n>
set size limit for inlining small routines
/Qinline-min-size-
no size limit for inlining small routines
/Qinline-max-size:<n>
set size limit for inlining large routines
/Qinline-max-size-
no size limit for inlining large routines
/Qinline-max-total-size:<n>
maximum increase in size for inline function expansion
/Qinline-max-total-size-
no size limit for inline function expansion
Компилятор имеет массу опций, которые управляют инлайнингом, но как легко убедится в основном это опции регулирующие допустимое увеличение размера кода.
/Qinline-max-per-routine:<n>
maximum number of inline instances in any function
/Qinline-max-per-routine-
no maximum number of inline instances in any function
/Qinline-max-per-compile:<n>
maximum number of inline instances in the current compilation
/Qinline-max-per-compile-
no maximum number of inline instances in the current compilation
/Qinline-factor:<n>
set inlining upper limits by n percentage
/Qinline-factor-
do not set set inlining upper limits
/Qinline-forceinline
treat inline routines as forceinline
/Qinline-dllimport
allow(DEFAULT)/disallow functions declared __declspec(dllimport) to be inlined
/Qinline-calloc directs the compiler to inline calloc() calls as malloc()/memset()
Клонирование функций
Если в функцию f(x,y,x) передается x=2 в одном случае и x=3 в другом, то возможно заменить вызов функции f на вызовы f2 и f3.
Частичная подстановка
Если в функции f содержится вначале f.
Девиртуализация для C++
Вызов функций через указатели дороже простого вызова.
C++ - объектно-ориентированный язык поддерживающий высокий уровень абстракции.
Возможность выполнения методов функции в зависимости от типа объекта времени выполнения.
A => B => C
Все наследуемые классы переопределяют virtual int foo()
int process(class A *a) {
return(a->foo());
}
Еще один пример полезной IP оптимизации
Одной из проблем затрудняющей работу
Одной из межпроцедурных оптимизаций является девиртуализация функций. Функцию можно девиртуализовать, если в программе не используются объекты родительского класса.

Определение выгодности оптимизаций
невыгодно сохранять общее
z=x*y;
if(почти_никогда) {
t=x*y;
}
невыгодно выносить инвариант из цикла, если он может ни разу не использоваться при расчетах
for(i=0;i<n;i++) {
…
if(почти_никогда) {
t = invariant; break; }
}
| выгодно группировать вместе наиболее "часто" используемые фрагменты программы |
| невыгодно выполнять подстановку функции, которая "редко" вызывается в процессе выполнения программы. |
Сейчас я сформулирую несколько общих соображений, которые вроде бы очевидно вытекают из общих принципов улучшения производительности, которые мы рассматривали в предыдущих лекциях.
| выгодно комбинировать вместе "часто" совместно используемые элементы структуры |
| невыгодно тратить время на оптимизацию редко используемых фрагментов кода. |
Статистический профилировщик вычисляет вероятность условных переходов и вес базовых блоков. Это делается на основе анализа исходного кода. Межпроцедурный анализ пытается рассчитать вес процедур на основе анализа
Анализ исходного кода не может обеспечить точное вычисление весовых характеристик. В общем случае, не известны входные данные исполняемой программы, время вычисления ограничено.
Существует встроенная функция builtin_expect предназначенная для передачи компилятору информации о вероятности ветвления.
if(x) => if(__builtin_expect(x,1))
Т.е. При определении выгодности оптимизаций большую роль играет вероятность того или иного события а так-же "условный вес" того или иного базового блока. Эту информацию предоставляет статический профилировщик (static profiler). Его задача на основе анализа кода программы оценить вероятность того или иного перехода, частоту выполнения инструкции, частоту использования полей структуры и т.д. На основании оценок сделанных профилировщиком подставляются функции, оптимизируются переходы и делаются
В качестве примера можно рассмотреть вопрос определения вероятности того, что фигура на доске является пешкой. Статистический профилировщик вычисляет эту вероятность на основе анализа перечня всех имеющихся фигур.
Динамический профилировщик собирает весовые характеристики на основе анализа статистики собранной при запуске инструментированного приложения. Инструментация в данном случае – снабжение приложения механизмами для сбора статистики.
/Qprof-gen[:keyword]
instrument program for profiling.
Optional keyword may be srcpos or globdata
/Qprof-use[:<arg>]
enable use of profiling information during optimization
weighted - invokes profmerge with -weighted option to scale data
based on run durations
[no]merge - enable(default)/disable the invocation of the profmerge
tool
Динамический профилировщик работает иначе. Сначала подготавливается специальная программа с профилировкой, т.е. в нее вставляются различные счетчики. Затем программа запускается на обычном для нее наборе данных и собирает информацию о выполнении в специальный файл. Затем осуществляется перекомпиляция данной программы с учетом собранной статистики. Это позволяет лучше оптимизировать переходы, подставить функции, а также лучше распараллелить и завекторизовать циклы.
Последовательность действий при использовании динамического
/Qprof_gen/Qprof_use для использование собранных статистик при компиляции.Даже если вы используете очень разные данные, тем не менее динамический профилировщик в состоянии улучшить статистику для многих переходов внутри программы.
Информация собранная динамическим профилировщиком более точная, поэтому некоторые оптимизации, применение которых может дать большой негативный эффект при неправильном применении могут выполняться только при наличии информации от динамического
Некоторые оптимизации которые базируются на профилировочной информации:
| перестановки базовых блоков |
| группировка часто используемых функций |
| вынос "холодных" базовых блоков за |
Динамическое выделение памяти
Объекты и массивы могут инициализироваться динамически во время исполнения приложения с использованием операторов new и delete, malloc и free. Менеджер памяти, это часть приложения, обрабатывающая запросы на выделение и освобождение памяти.
Типичные ситуация, когда динамическое выделение памяти необходимо:
Неудобства
Менеджер памяти – это часть приложения, обрабатывающий запросы на выделение и освобождение памяти. Менеджер памяти или прилинковывается статически в момент линковки или может вызываться из динамической библиотеки. Одним из показателей качества работы менеджера памяти является быстрота предоставления необходимой памяти, а так же быстрота освобождения адресов и умение объединять неиспользованные блоки. В процессе длительной работы приложения качество используемой памяти может ухудшаться.
Помимо этого менеджер памяти может оказывать значительное влияние на производительность приложения.
Одной из значимых проблем для производительности современных программ, написанных на C и C++, которые работают с
Определяет влияние менеджера памяти на производительность компактность размещения в памяти совместно используемых объектов. В этом случае больше вероятность, что при обращении к объекту он окажется в кэш-памяти.
Важно оценить сильные и слабые стороны использования динамической памяти при проектировании приложения.
Различные менеджеры памяти реализуют различные алгоритмы выделения памяти и пытаются снизить затраты на работу с динамической памятью.
Альтернативные менеджеры памяти:
| Smartheap |
| dlmalloc |
Одним из важных факторов производительности в C++ является компактность размещения в памяти совместно используемых объектов, объединенных в различные связные списки. Связный список менее эффективен чем
В случае работы с массивами непрерывный массив также выгоднее чем массив указателей по причине лучшей работы системы памяти.
При выделении памяти для динамических объектов память выделяется случайным образом и вероятность того, что например элементы списка будут хорошо располагаться в памяти очень невелика. Чтобы решить эту проблему менеджеры памяти пытаются использовать различные методы, например сортируют выделяемые блоки по размеру и т.д.
Связные списки в памяти
| Связный список: | ![]() |
| Может располагаться в памяти так: | ![]() |
| И в физической памяти: | ![]() |
При работе со связными списками, в том случае если мы аллоцируем память для каждого последующего объекта с помощью динамического менеджера памяти, память может выделиться достаточно произвольным образом внутри виртуальной памяти. В случае работы с многопоточным приложением на многопроцессорной машине память для элементов списков может быть выделена на физической памяти разных процессоров.

Идея данного примера состоит в замене умолчательного выделения памяти для массива указателей на выделении памяти из одного предварительно аллоцированного большого блока памяти. В этом случае работа с массивом указателей будет идентична работе с непрерывным массивом.
Контейнеры
Существует популярный способ улучшения работы с динамически выделяемой памятью при помощи контейнеров.
Создание и использование контейнеров это один из примеров эффективного использования шаблонов (template) в С++. Наиболее распространенный набор контейнеров предоставляется Standard Template Library (STL), которая поставляется с современными C++ компиляторами.
Однако STL в основном разрабатывалась для гибкости использования и вопросы производительности имели более низкий приоритет. Поэтому выделение памяти для хранения объектов осуществляется пошагово по мере роста количества объектов, которые необходимо хранить в памяти. Отсутствует гибкая система, позволяющая заранее определять сколько памяти необходимо выделить изначально. В случае расширения контейнера может возникнуть необходимость копирования содержания контейнера. Это копирование происходит с использованием конструкторов копирования.
Еще один популярный метод – это метод пулов памяти. Одно из его отличий состоит в том, что при расширении пула весь блок памяти копируется с помощью memcpy.

Кодогенератор
Кодогенерация — часть процесса компиляции, когда специальная часть компилятора, кодогенератор, конвертирует синтаксически корректную программу в последовательность инструкций, которые могут выполнятся на машине. При этом могут применятся различные, в первую очередь машинно-зависимые оптимизации. Часто кодогенератор является общей частью для множества компиляторов, каждый из которых генерирует промежуточный код, подаваемый на вход кодогенератору.
Планирование инструкций (instruction scheduling)
Планирование инструкций это компьютерная оптимизация, используемая для улучшения уровня инструкционного параллелизма. Оптимизация обычно осуществляется за счет изменения порядка инструкций для уменьшения задержек процессорного конвейера.
Процессор имеет собственный механизм для планирования инструкций и распределения их по исполняющим устройствам. Этот механизм предусматривает упреждающий просмотр поступающих инструкций. Но он может быть недостаточно эффективен поскольку "окно упреждающего просмотра" ограничено. Инструкции могут переставляться в соответствии со следующими соображениями:
Планирование инструкций может производиться внутри одного базового блока или внутри суперблока, объединения нескольких базовых блоков. Некоторые инструкции могут передвигаться за границы соответствующих им базовых блоков.
Планирование инструкций может осуществляться как до, так и после распределения регистров.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.