Оптимизирующий компилятор
Download
1 / 36

Оптимизирующий компилятор - PowerPoint PPT Presentation


  • 186 Views
  • Uploaded on

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

loader
I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.
capcha
Download Presentation

PowerPoint Slideshow about ' Оптимизирующий компилятор' - domani


An Image/Link below is provided (as is) to download presentation

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -
Presentation Transcript

Основные характеристики приложения, влияющие на его производительность:

  • Эффективность вычислений.

  • Эффективность работы с памятью.

  • Правильное предсказание переходов.

  • Эффективность использования векторных инструкций.

  • Эффективность параллелизации.

  • Уровень инструкционного параллелилизма.


Место и роль компилятора приложения, влияющие на его производительность:

Компилятор — транслятор, который осуществляет перевод всей исходной

программы в эквивалентную ей результирующую программу на языке машинных

команд или на языке ассемблера.

Основная задача оптимизирующего компилятора – получение кода максимально

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

С точки зрения разработчика программа должна быть:

  • Легко читаемой и модифицируемой.

  • Легко отлаживаемой.

  • Быстро исполняемой.

    Разработчику необходима

  • надежная унифицированная среда разработки;

  • возможность варьировать уровни отладки и быстродействия;

  • возможность получать высокоэффективный код для различных операционных систем и микропроцессорных архитектур.

    Компилятор должен удовлетворить эти требования.


Оптимизирующий компилятор приложения, влияющие на его производительность:

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

Возникают следующие проблемы:

Сложность доказательства допустимости тех или иных оптимизаций.

Сложность расчет выгодности оптимизаций.

Отсутствие во время компиляции представления о типичных входных данных.

Для достижения хороших результатов требуется тесное сотрудничество с разработчиком.

Чтобы использовать умело средства компилятора, программист должен:

иметь представления об архитектуре, на которой будет использоваться его программа;

ознакомиться с настройками компилятора;

ознакомиться с основными техниками улучшения производительности, которые использует компилятор;

ознакомиться с основными проблемами, вызывающими замедление работы программы;

знать примерные данные, с которыми будет работать программа;

уметь пользоваться инструментами для анализа производительности программы.


Intel
Компиляторы приложения, влияющие на его производительность:Intel

С/C++ и Fortran для

операционных систем Windows,

Linux и Mac OS

Для Windows компилятор может

быть интегрирован в Microsoft Visual

Studio

Главной целью корпорации

является высокая

производительность компиляторов

и совместимость с Microsoft Visual

Studio на Windows и с gcc на Linux и

Mac OS.

www.intel.com/software/products


Исходные файлы приложения, влияющие на его производительность:

FE (C++/C или Fortran)

Архитектура компилятора

Внутреннее представление

Профилировщик

Временный

файл или

Obj с ВП

Скалярные оптимизации

HPO

IP/IPO оптимизации

Генератор кода

Скалярные оптимизации

Обьектные файлы

HPO

Генератор кода

Исполняемый файл

Библиотека


Front end
Front End приложения, влияющие на его производительность:

  • Синтаксический анализ (parsing) — это процесс анализа входной последовательности символов с целью разбора грамматической структуры, обычно в соответствии с заданной формальной грамматикой.

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

  • Обычно синтаксический анализ делится на два уровня:

  • Лексический анализ — входной поток символов разбивается на линейную последовательность токенов— «слов» языка (напр. целые числа, идентификаторы, строковые константы и т. д.);

  • Семантический анализ— из токеноввыделяются «предложения» языка согласно грамматическим правилам, и создается дерево разбора.

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


Внутреннее представление. (Дерево разбора)

Процесс лексического разбора (parsing) – это процесс создания некоторого внутреннего представления компилятора.

Внутреннее представление компилятора - это различные структурированные данные, связанные друг с другом.

Базовой единицей является лист утверждений (statements)

Утверждения используются для отображения присваиваний, команд управления потоком (таких как IF,GOTO,CALL,RETURN), Phi-statements для SSA, вызовов функций и т.д.


Внутреннее представление. (Дерево разбора)

Утверждения (statements) обычно представлены в виде спискаи могут быть связаны двумя способами:

1) Лексически.

Каждое утверждение имеет предшественника (predecessor) и потомка (successor).

2) Графом потока управления.

struct STMT {

common_members:

int type;

STMT * pred;

STMT *succ;

BBLOCK bblock;

… }

Union {

stmt_1_members;

stmt_2_members;

… }

}

Макросы помогают получать доступ к различным полям данной структуры.

Оптимизации часто базируются на полном проходе через весь лист утверждений.Например:

FOR_ALL_ENTRY_STMT(entry,stmt) {

if(STMT_type(stmt) == STMT_ASSIGN {

//обработка присваиваний

}

}


STMT_ASSIGN (Дерево разбора)

EXPR_VAR

lval

rval

‘a’

EXPR_ADD

EXPR_VAR

EXPR_VAR

‘b’

‘c’

Выражения

a = b + c;

Выражения (expressions) представляют собой дерево выражений.

Граничные выражения, то есть те, которые находятся на конце дерева выражений, могут быть:переменной (ссылка на область памяти), значением (константа).


Переменная и ее свойства (Дерево разбора)

  • имя

  • информация о классе (для членов класса)

  • размер в памяти

  • выравнивание (alignment)

  • тип (например, ссылка на элемент из таблицы типов)

  • размер (для массива)

  • указатель на описание структуры массива

  • указатель на родительскую структуру (для полей структуры)

  • область хранения (локальная, статическая, глобальная)

  • область видимости

    Атрибуты:

  • является ли элементом объединения

  • брался ли адрес этой переменной

  • тип доступа (для членов класса)

  • специальнаяконструкция Fortran

  • имеет специальные атрибутыдля Fortran

  • является аргументом

  • переменная отмечена как совместноиспользуемая в OPENMP директиве

    Многие оптимизации и компоненты компилятора для ускорения работы заводят специальные ссылки и атрибуты используемые только ими.


Функции и их свойства (Дерево разбора)

  • имя;

  • указатель на родительский класс;

  • выравнивание (alignment);

  • ссылки на тело функции;

  • Call convention (Для разных OS);

  • Intrinsic (библиотечная функция компилятора);

  • Область видимости.

    Атрибуты:

  • функция не имеет «побочных эффектов»;

  • не возвращает значение;

  • обязательна для подстановки (inline);

  • конструктор класса и т.д.


Граф потоков управления ( (Дерево разбора)Control Flow Graph)-

представление всех путей, которые могут быть пройдены в процессе выполнения программы.

Базовым узлом этого представления является базовый блок (basic blocks). Это непрерывные части кода без переходов и меток переходов. Метка перехода начинает такой блок, переход его завершает.

Некоторые определения:

Блок входа (entry block) – это блок, через который все потоки управления входят в граф.

Блок выхода (exit block) – блок, через который все потоки управления покидают граф.


Граф потока управления (Дерево разбора)

  • int main() {

  • int sum=0;

  • int i=1;

  • while (i<11) {

  • sum=sum+i;

  • i = i+1;

  • }

  • printf(“%d\n”,sum);

  • }

Entry

Sum=0;

i=1;

L12:

if (i<11)

sum = sum+i;

i = i+1;

Goto L12

printf(..)

Return


Построение (Дерево разбора)

осуществляется в два прохода по списку утверждений:

1) Находим базовые блоки

  • Первое утверждение начинает базовый блок.

  • Каждое утверждение, которое является целью перехода, начинает базовый блок.

  • Каждое утверждение следующее за переходом начинает базовый блок.

    2) Проходим повторно и связываем базовые блоки связями или гранями (edge).

Struct BBLOCK {

STMT first_stmt

STMT last_stmt

BBLOCK_LIST pred_list

BBLOCK_LIST succ_list

}


Обработка графа потока управления

После определения базовых блоков появляется возможность обходить все утверждения программы с использованием CFG.

Например:

FOR_ALL_ENTRY_BBLOCK(entry,bblock) {

FOR_ALL_BBLOCK_STMT(bblock,stmt) {

// обрабатываются утверждения из одного базового блока

IF(STMT_type(stmt) == STMT_ASSIGN) {

EXPR lopnd;

lopnd=STMT_lval(stmt);

if(EXPR_type(lopnd) == EXPR_VAR) {

// Обработать присваивание переменной

}

}

}

}


Исходные файлы управления

FE (C++/C или Fortran)

Архитектура компилятора

Внутреннее представление

Профилировщик

Временный

файл или

Obj с ВП

Скалярные оптимизации

HPO

IP/IPO оптимизации

Генератор кода

Скалярные оптимизации

Обьектные файлы

HPO

Генератор кода

Исполняемый файл

Библиотека


Скалярные оптимизации управления

  • Свертка констант, протяжка констант, протяжка копий (Constant folding, constant propagation, copy propagation)

  • Свертка констант - процесс вычисления констант во время компиляции.

  • Протяжка констант – подстановка величин известных констант в выражение

  • intx = 14;

  • inty = 7 - x / 2;

  • => constant propagation =>

  • int x = 14;

  • int y = 7 – 14 / 2;

  • Протяжка копий – процесс замены переменных их значениями

  • y = x;

  • z = 3 + y

  • => copy propagation =>

  • z = 3 + x


  • Скалярные оптимизации управления

    Удаление повторных вычислений (Common subexpression elimination) – поиск идентичных подвыражений и сохранение результата вычисления во временной переменной для последующего повторного использования.

    a = b * c + g;

    d = b * c * d;

    => CSE =>

    tmp = b * c;

    a = tmp + g;

    d = tmp * d;


  • Скалярные оптимизации управления

    Удаление мертвого кода (Dead code elimination) это удаление кода, который не изменяет выходных данных программы. К мертвому коду относится код, который никогда не выполняется или изменяет только не влияющие на результат переменные.

    intfoo() {

    inta = 24;

    intb = 25; /* Присвоение не влияющей на результат переменной */

    intc;

    c = a << 2;

    returnc;

    b = 24; /* Недостижимый код */ }

    Мертвый код может появится после многих оптимизаций компилятора, после протяжки констант и копий, после прямой подстановки (inlining) и т.п.


  • Скалярные оптимизации управления

  • Удаление излишнего ветвления, протяжка условий

  • Удаляются блоки кода, которые не могут быть достижимы из-за цепочки условных ветвлений.

  • if(x>0) {

  • if(x>0) { a=x; }

  • } else { a=-x; }

  • }

  • =>

  • if(x>0) {

  • a=x;

  • }

  • Также может возникнуть из-за скалярных оптимизаций или прямой подстановки.


Data flow analysis
Анализ потоков данных ( управленияData Flow Analysis)

сбор информации о возможном наборе значений переменных, вычисляемых в различных точках программы.

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

Граф определения/использования (definition-use graph) – это граф, который содержит дуги из каждой точки определения переменной в программе к каждой точке ее использования.


Построение управления

Построение цепочек def-use для базового блока тривиально. Каждое определение переменной связано со всеми последующими ее использованиями. Каждое последующее определение прекращает предыдущую цепочку и начинает новую.

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

Uses(b): Переменные, которые используются в блоке, но не имеют определений внутри блока.

Defsout(b): Переменные, которые были определены в b и достигли конца блока.

Killed(b): Переменные, определения которых были отменены внутри блока другими определениями.

Reaches(b): Переменные, определённые в других блоках, включая b, которые могут достичь b.


Т.е. для понимания того, какие определения будут использоваться внутри базового блока, важно знать reaches(b).

Можно построить итерационный процесс, который будет вычислять reaches(b) через перечисленные множества предыдущих блоков.

Reaches(b) = U для всех предшественников (defsout(p) U (reaches(p) ∩ ¬killed(p))

Проблема в том, что при наличии циклов, блок reaches(b) может зависеть от reaches(b). Утверждается, что многократно повторяя это вычисление в каждом базовом блоке CFG,можно получить окончательное решение.


  • Опираясь на построенное дерево, можно делать многие оптимизации. Например, удалять мертвый код и протягивать константы. Главная проблема такого подхода - большое количество дуг в Def-Use графе и большое время расчета этого дерева.

S1 X=

S2 X=

S3 X=

Пример иллюстрирует эту проблему. Определения в S1, S2, S3 проходят через вершину S4. Поскольку каждое определение достигает каждого использования, то в данном частном случае возникает 9 дуг. Для того, чтобы решить эту проблему, была предложена SSA форма.

S4

S7 =X

S5 =X

S6 =X


Ssa static single assignment form
SSA (Static single assignment form) можно делать многие оптимизации. Например, удалять мертвый код и протягивать константы. Главная проблема такого подхода - большое количество дуг в

  • SSA форма предполагает уникальное имя для каждогоопределения переменной и введение специальных псевдо-присваиваний.

S2

S1

S3

X1=

X2=

X3=

X4=φ(X1,X2,X3)

S4

S5

S7

S6

=X4

=X4

=X4


SSA можно делать многие оптимизации. Например, удалять мертвый код и протягивать константы. Главная проблема такого подхода - большое количество дуг в призвана избавить разработчиков от необходимости строить сложные use/def цепочки для локальных переменных. Сила SSA заключается в том, что каждая переменная имеет только одно определение внутри программы. Поэтому use/def цепочка очевидна. SSA представление вводит специальные Phi-функции в местах, в которых возникает неопределенность, для создания новой переменной. Это так называемые псевдо-присваивания.

При построении необходимо расставить Phi – функции и породить новые уникальные переменные.

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

Для того, чтобы правильно вставить Phi функции, нужно рассмотреть некоторые понятия теории графов.


Доминатор – узел можно делать многие оптимизации. Например, удалять мертвый код и протягивать константы. Главная проблема такого подхода - большое количество дуг в N - доминирует над узлом M,если все пути к М идут через N.

Узел N непосредственный доминатор M,если он последний доминатор на любом пути от входа до M.

Границей доминирования (Dominance frontier) - узла x называется множество всех таких узлов w, что x доминирует над узлами, являющиеся предками узла w, но не является строгим доминатором узла w.

Другими словами, это граница между доминируемыми и недоминируемыми узлами.

Пример:

Dom[5] = {5,6,7,8}

DF[5] ={5,4,12,11}

1

2

5

5

9

3

7

6

6

7

10

4

11

8

8

11

12


В можно делать многие оптимизации. Например, удалять мертвый код и протягивать константы. Главная проблема такого подхода - большое количество дуг в SSA форме каждое определение переменной должно доминировать над использованием переменной.

  • Построение множества доминаторов для каждого базового блока.

    Правило: Множество доминаторов для узла N есть пересечение множества доминаторов всех его предшественникови сам узел (множество доминаторов вершины содержит ее саму).

    Строгий доминатор N, это доминатор !=N. Непосредственный доминатор – это ближайший узел из множества доминаторов.

    idom(N) – непосредственный доминатор базового блока N

    children(N) – множество базовых блоков для N, которые он доминирует

2

3

4

5

6


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

если блок N содержит определение переменной a, то всякий узел на границе доминирования узла N требует Phi функции для a. Каждая Phi функция - это тоже определение, поэтому необходимо применять критерий границы доминирования до тех пор, пока ни один блок более не требует вставки Phi функции.

A_2=φ(A_1,A_3)

B=A_2

A_3=x

B=A

A=x

Вставка φ функции для вершины 5 из схемы на слайде 20.


2.) можно делать многие оптимизации. Например, удалять мертвый код и протягивать константы. Главная проблема такого подхода - большое количество дуг в Введем 2 множества:

DFlocal[n]: множество потомков узла n, для которых n не является строгим доминатором (Этот признак легко определить для узла n.)

DFup[n]: множество вершин в границе доминирования n, не доминируемыx непосредственным доминатором n (нужно знать DF(n) чтобы построить это множество)

Тогда: DF [n] = DFlocal[n] U (Uc DFup[c]),

Uc – объединение по с из children [n] – узлы, непосредственным доминатором которых является узел n.

Из этой формулы видно, что если узел не доминирует другие узлы (children[n] ={}) – вычисление DF – тривиальная задача.

1

2

children[8]={};DF[8]={5,12}; Idom[8]={5}; DFup[8]={5,12}

children[6]={}; Idom[6]={5} ; DFlocal[6] = {4,8};

DFup[6]={4} так как Idom[8]={5}

chilren[7] ={}; Idom[7]={5}; DFlocal[7]={8,11}; DFup[7]={11} так как Idom[8]={5}

5

5

9

3

6

6

7

10

4

8

8

DF[5]={}U(DFup[6]UDFup[7]UDFup[8])

DF[5]={}U{4}U{11}U{5,12}={4,5,12,11}

11

12


  • Алгоритм расчёта границы доминирования

    computeDF(n) {

    s <- {};

    foreach(y from succ[n])

    if(idom(y) !=n ) s U {y};

    foreach(c from children[n]) {

    computeDF(c);

    foreach(w from DF(c))

    if(!(n dom w)) s U {w};

    }

    DF[n]=s;


1 доминирования

A=X (A from Def(3))

2

3

4

5

Необходима ли φ функция для A в узле 6?

6

Функция φ необходима, поскольку при обработке определения А в узле 3 мы вставляем в узел 5 φ функцию для A, а она также является определением.


Алгоритм вставки доминированияPhi функций

Aorig[n] – множество определяемых переменных блока n

foreach (n)

foreach(a from Aorig[n])

defsites[a] =defsites[a] U {n}

Определили все базовые блоки, где определяется переменная a.

foreach(a) {

w =defsites[a];

while(w isn’t empty) {

take n from w;

foreach (Y from DF[n]) {

if(Y !from Aφ[n]}){

insert a=φ(a1,…,ak) at top of Y;

Aφ[n] = Aφ[n] U {Y};

}

if(Y !from Aorig[n])

w = w U {Y};

}

После этого обходится дерево доминирования, и выставляются правильные номера версий для переменных.


  • Оптимизации, с использованием доминированияSSA формы

    Удаление мертвого кода. (Dead code elimination)

    Если переменная a_verне используется, то на удаляется.

    Продвижение констант(Constant propagation)

    Если в коде программы есть присвоение a_ver=const, то все использования a_verзаменяются на const.

    Если в коде программы есть a_next=φ(с,c), заменяем φ на с.

    Продвижение копий (Copy propagation)

    Если в коде программы есть присвоение a_n=b_k, заменяем все использования a_nна b_k.

    Если в коде программы есть присвоение a_n=φ(b_k,b_k), заменяем a_nна b_k.


Спасибо за внимание! доминирования