Опубликован: 10.10.2011 | Доступ: свободный | Студентов: 1428 / 454 | Оценка: 4.31 / 4.16 | Длительность: 05:32:00
Специальности: Программист
Лекция 4:

Глобальные и локальные оптимизации

< Лекция 3 || Лекция 4: 123 || Лекция 5 >

Определение выгодности оптимизаций

невыгодно сохранять общее подвыражение во временной переменной если "часто" используется только одно подвыражение

   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
    

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

Последовательность действий при использовании динамического профилировщика:

  1. построить приложение с инструментацией /Qprof_gen
  2. запустить инструментированное приложение с представительным набором данных, т.е. одним или несколькими наборами наиболее часто используемых данных. Информация добавляется в файл со статистиками.
  3. пересобрать приложение с опцией /Qprof_use для использование собранных статистик при компиляции.

Даже если вы используете очень разные данные, тем не менее динамический профилировщик в состоянии улучшить статистику для многих переходов внутри программы.

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

Некоторые оптимизации которые базируются на профилировочной информации:

перестановки базовых блоков
группировка часто используемых функций
вынос "холодных" базовых блоков за пределы функции (расщепление функций)

Динамическое выделение памяти

Объекты и массивы могут инициализироваться динамически во время исполнения приложения с использованием операторов new и delete, malloc и free. Менеджер памяти, это часть приложения, обрабатывающая запросы на выделение и освобождение памяти.

Типичные ситуация, когда динамическое выделение памяти необходимо:

  • Необходимо создать большой массив размер которого неизвестен во время компиляции.
  • Массив может быть очень большим для того чтобы расположить его на стеке.
  • Объекты должны создаваться во время работы программы, но количество необходимых объектов неизвестно.

Неудобства динамического выделения памяти:

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

Менеджер памяти – это часть приложения, обрабатывающий запросы на выделение и освобождение памяти. Менеджер памяти или прилинковывается статически в момент линковки или может вызываться из динамической библиотеки. Одним из показателей качества работы менеджера памяти является быстрота предоставления необходимой памяти, а так же быстрота освобождения адресов и умение объединять неиспользованные блоки. В процессе длительной работы приложения качество используемой памяти может ухудшаться.

Помимо этого менеджер памяти может оказывать значительное влияние на производительность приложения.

Одной из значимых проблем для производительности современных программ, написанных на C и C++, которые работают с динамическими объектами является работа с менеджером памяти. В обычном случае, когда память для объектов выделяется и освобождается "на лету" и программа работает с элементами различных списков, очень сложно добиться хорошего попадания в кэш память. Существуют разные подходы к решению этой проблемы. К сожалению, компилятор имеет очень скромные возможности, для того чтобы помочь пользователю.

Определяет влияние менеджера памяти на производительность компактность размещения в памяти совместно используемых объектов. В этом случае больше вероятность, что при обращении к объекту он окажется в кэш-памяти.

Важно оценить сильные и слабые стороны использования динамической памяти при проектировании приложения.

Различные менеджеры памяти реализуют различные алгоритмы выделения памяти и пытаются снизить затраты на работу с динамической памятью.

Альтернативные менеджеры памяти:

Smartheap
dlmalloc

Одним из важных факторов производительности в C++ является компактность размещения в памяти совместно используемых объектов, объединенных в различные связные списки. Связный список менее эффективен чем линейный массив по следующим причинам:

  • Каждый объект создаётся отдельно. Выделение и освобождение объекта имеет свою цену.
  • Объекты в памяти хранятся не последовательно. При обходе связного списка вероятность попадания в кэш снижается. Процессорная предвыборка данных неэффективна.
  • Необходима дополнительная память для хранения ссылок и информации о выделенных блоках памяти.

В случае работы с массивами непрерывный массив также выгоднее чем массив указателей по причине лучшей работы системы памяти.

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

Связные списки в памяти

Связный список:

Может располагаться в памяти так:

И в физической памяти:

При работе со связными списками, в том случае если мы аллоцируем память для каждого последующего объекта с помощью динамического менеджера памяти, память может выделиться достаточно произвольным образом внутри виртуальной памяти. В случае работы с многопоточным приложением на многопроцессорной машине память для элементов списков может быть выделена на физической памяти разных процессоров.


Идея данного примера состоит в замене умолчательного выделения памяти для массива указателей на выделении памяти из одного предварительно аллоцированного большого блока памяти. В этом случае работа с массивом указателей будет идентична работе с непрерывным массивом.

Контейнеры

Существует популярный способ улучшения работы с динамически выделяемой памятью при помощи контейнеров.

Создание и использование контейнеров это один из примеров эффективного использования шаблонов (template) в С++. Наиболее распространенный набор контейнеров предоставляется Standard Template Library (STL), которая поставляется с современными C++ компиляторами.

Однако STL в основном разрабатывалась для гибкости использования и вопросы производительности имели более низкий приоритет. Поэтому выделение памяти для хранения объектов осуществляется пошагово по мере роста количества объектов, которые необходимо хранить в памяти. Отсутствует гибкая система, позволяющая заранее определять сколько памяти необходимо выделить изначально. В случае расширения контейнера может возникнуть необходимость копирования содержания контейнера. Это копирование происходит с использованием конструкторов копирования.

Еще один популярный метод – это метод пулов памяти. Одно из его отличий состоит в том, что при расширении пула весь блок памяти копируется с помощью memcpy.


Кодогенератор

Кодогенерация — часть процесса компиляции, когда специальная часть компилятора, кодогенератор, конвертирует синтаксически корректную программу в последовательность инструкций, которые могут выполнятся на машине. При этом могут применятся различные, в первую очередь машинно-зависимые оптимизации. Часто кодогенератор является общей частью для множества компиляторов, каждый из которых генерирует промежуточный код, подаваемый на вход кодогенератору.

  • Конвертация утверждений и выражений внутреннего представления в инструкции процессора данной архитектуры.
  • Специфические архитектурные оптимизации.
    • Удаление ветвления с помощью условных присваиваний.
  • Подставляет тела простейших встроенных функций.
  • Выравнивает базовые блоки в памяти.
  • Подготовка вызовов процедур, т.е. загрузка необходимых переменных в регистры и/или на стек для передачи в вызываемую процедуру.
  • То же самое для вызываемой процедуры. Выделение места на стеке для локальных переменных. Сохранение и восстановление используемых внутри процедуры регистров.
  • Планирование инструкций.
    • Планирование переходов.
    • Учет задержки обращения к памяти.
  • Распределение регистров.
  • Вычисление дистанций для переходов

Планирование инструкций (instruction scheduling)

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

Процессор имеет собственный механизм для планирования инструкций и распределения их по исполняющим устройствам. Этот механизм предусматривает упреждающий просмотр поступающих инструкций. Но он может быть недостаточно эффективен поскольку "окно упреждающего просмотра" ограничено. Инструкции могут переставляться в соответствии со следующими соображениями:

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

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

Планирование инструкций может осуществляться как до, так и после распределения регистров.

< Лекция 3 || Лекция 4: 123 || Лекция 5 >