LLVM: Как приручить дракона?
Дмитрий Каш цын, HDSoftии
Как работает компилятор
1. Прочитать текст программы из файла
2. Записать исполняемый файл
Чуть детальнее
● Разбор лексем и синтаксический анализ
● Построение AST программы
● Преобразования (магия)
● Генерация объектного файла
● Линковка исполняемого файла
4
Но зачем?!*
* http://www.linux.org.ru/gallery/workplaces/10931314
5
Если серьезно
● Развитие computer science не стоит на месте
● Новые идеи — новые языки
● Новые языки — новые компиляторы
● Успех Rust и Go говорит сам за себя
6
Мой уютный компилятор™
● Парсинг исходников
● Представление программы?
● Оптимизация всего и вся
● Целевая архитектура, наборы инструкций
● Аллокация и распределение регистров
● Интероперабельность — системные вызовы,
FFI, работа с библиотеками
● Генерация исполняемого файла, линковка
● Отладочная информация
7
Структура команды x86 (OMG)
http://penberg.blogspot.ru/2010/04/short-introduction-to-x86-instruction.html
8
Как угодить всем?
● Языки разные. Очень.
● Разное отношение к данным
● Разные модели памяти
● Разные целевые архитектуры
● Разные наборы инструкций
9
Решение LLVM
● Запись программы в виде, не зависящем от
всего вышеперечисленного
● Промежуточное представление — IR код
● Обобщенная система команд
10
Решение LLVM
● Запись программы в виде, не зависящем от
всего вышеперечисленного
● Промежуточное представление — IR код
● Обобщенная система команд
Внезапно: LLVM — это не VM o_O
11
Разбор исходного текста
● LLVM считает, что AST уже есть
● Чтобы его получить, нужно провести
лексический и синтаксический анализ
● К счастью, это не надо делать вручную
● На помощь придут
– Flex/Bison
– ANTLR
– И другие
12
Так видит программу человек
int gcd(int a, int b) {
while (b != 0) {
if (a > b)
a = a − b;
else
b = b − a;
}
return a;
}
13
Так видит программу компилятор
condition
body
else-bodyif-body
while
variable
name: b
constant
value: 0
compare
op: ≠
branch
compare
op: >
assign
bin op
op: −
assign
bin op
op: −
statement
sequence
return
variable
name: a
variable
name: a
variable
name: a
variable
name: a
variable
name: a
variable
name: b
variable
name: b
variable
name: b
variable
name: b
condition
14
Лексический анализ
● Первичный разбор текста программы
● Удаление лишнего шума
● Преобразование потока входных символов в
последовательность токенов
15
Лексический анализатор flex
● Позволяет записать правила разбора в
текстовом, человеко-читаемом виде
● Регулярные выражения — это сила
● На выходе — токены
16
Входной файл для flex (огрызок)
whitespace [ rnt]*
number [0-9]+
identifier [a-zA-Z]+
{whitespace} { /* ignore whitespaces */ }
{number} { sscanf(yytext, "%d", &yylval->value);
return TOKEN_NUMBER; }
"+" { return PLUS; }
"-" { return MINUS; }
"(" { return LPAREN; }
")" { return RPAREN; }
";" { return SEMICOLON; }
"," { return COMMA; }
"=" { return EQ; }
"!=" { return NEQ; }
">" { return GTR; }
"if" { return IFSYM; }
"while" { return WHILESYM; }
17
Синтаксический анализатор bison
● На вход получает поток токенов от лексера и
файл описания грамматики
● На основании правил грамматики
производит разбор
● На выходе дает древовидное представление
программы с которым может работать
компилятор
18
Входной файл для bison (огрызок)
input:
| input line;
line:
'n'
| exp 'n' { complete("result is %gn", $1); };
exp:
TOKEN_NUMBER { $$ = $1; }
| exp '+' exp { $$ = $1 + $3; }
| exp '-' exp { $$ = $1 - $3; }
| exp '/' exp { $$ = $1 / $3; }
| exp '*' exp { $$ = $1 * $3; }
| '(' exp ')' { $$ = $2; };
Позволяет разбирать арифметические выражения вида (2 + 3 * 4) * 5
19
IR код
● Intermediate Representation
● Запись графа управления в привычном текстовом виде…
20
IR код
● Intermediate Representation
● Запись графа управления в привычном текстовом виде…
но с плюшками:
– Asm-подобный синтаксис, но с параметризованными
функциями
– Строгая типизация
– Структуры данных, указатели
– Const, Volatile модификаторы
– Нотация SSA
21
Нотация SSA
● Static Single Assignment
● Переменных — нет о_О
● Все имена встречаются только единожды
● Функциональный стиль описания данных
● Императивный стиль описания операций
22
Что хорошего в SSA?
X ← 1
X ← 2
Y ← X
23
Что хорошего в SSA?
X ← 1
X ← 2
Y ← X
X1←1
X2←2
Y1←X2
24
Что хорошего в SSA?
X ← 1
X ← 2
Y ← X
X1←1 (RIP)
X2←2
Y1←X2
25
Что хорошего в SSA?
X ← 1
X ← 2
Y ← X
X1←1 (RIP)
X2←2 (RIP)
Y1←2
26
Граф потока управления*
● Control flow graph (CFG)
● Узлы — базовые блоки
● Ребра — инструкции
переходов
● Базовый блок —
линейный участок кода
(a)
(c)
(b)
(d)
* https://en.wikipedia.org/wiki/Control_flow_graph
27
Циклы и ветвления в SSA
28
Циклы и ветвления в SSA
29
Циклы и ветвления в SSA
30
Подсчет суммы массива
int sum_array(int* input, int length) {
int sum = 0;
for (int i = 0; i < length; ++i)
sum += input[i];
return sum;
}
31
Листинг IR кода
1 ; Function Attrs: nounwind readonly
2 define i32 @sum_array(int*, int)(i32* nocapture readonly %input, i32 %length) #0 {
3 %1 = icmp sgt i32 %length, 0 ; а есть вообще что суммировать?
4 br i1 %1, label %.lr.ph, label %._crit_edge
5 ._crit_edge:
6 %sum.0.lcssa = phi i32 [ 0, %0 ], [ %4, %.lr.ph ]
7 ret i32 %sum.0.lcssa ; возврат результата
8 .lr.ph:
9 %i.02 = phi i32 [ %5, %.lr.ph ], [ 0, %0 ]
10 %sum.01 = phi i32 [ %4, %.lr.ph ], [ 0, %0 ]
11 ; вычисление адреса текущего элемента в массиве и его загрузка в регистр
12 %2 = getelementptr inbounds i32, i32* %input, i32 %i.02
13 %3 = load i32, i32* %2, align 4
14 ; аккумулирование суммы и инкремент индекса
15 %4 = add nsw i32 %3, %sum.01 ; новое значение sum
16 %5 = add nuw nsw i32 %i.02, 1 ; новое значение i
17 ; условие выхода
18 %exitcond = icmp eq i32 %5, %length
19 ; проверка условия выхода и переход
20 br i1 %exitcond, label %._crit_edge, label %.lr.ph
21 }
32
Результат clang -O1 -m32
sum_array(int const*, int):
mov ecx, dword ptr [esp + 8]
xor eax, eax
test ecx, ecx
jle .LBB0_3
mov edx, dword ptr [esp + 4]
.LBB0_2: # %.lr.ph
add eax, dword ptr [edx]
add edx, 4
dec ecx
jne .LBB0_2
.LBB0_3: # %._crit_edge
ret
33
Результат clang -O1 (x64)
sum_array(int const*, int):
xor eax, eax
test esi, esi
jle .LBB0_2
.LBB0_1: # %.lr.ph
add eax, dword ptr [rdi]
add rdi, 4
dec esi
jne .LBB0_1
.LBB0_2: # %._crit_edge
ret
34
Оптимизации
● Наблюдаемое поведение
● Точки следования
● Эквивалентные преобразования
35
Основные идеи оптимизаций
● Не делать то, что никому не нужно
● Не делать дважды то, что можно сделать
один раз (а лучше не делать вообще)
● Если можно получить тот же результат, но
меньшими усилиями — это нужно сделать
● Сокращение издержек на всех уровнях
36
Виды оптимизаций
● Peephole оптимизации — буквально
«через замочную скважину». Локальные
оптимизации в пределах базового блока
● Внутрипроцедурные оптимизации
● Межпроцедурные оптимизации
● Оптимизации во время линковки
37
Loop Invariant Code Motion (LICM)
● Вынос инвариантных значений за пределы цикла
● Перенос значений из тела цикла в базовый блок возврата
int sum_array(int* input, int length) {
int sum = 0;
for (int i = 0; i < length; ++i) {
if (sum > length * 1024 * 1024)
printf("note: limit exceeded");
sum += input[i];
}
return sum;
}
38
Loop Invariant Code Motion (LICM)
● Вынос инвариантных значений за пределы цикла
● Перенос значений из тела цикла в базовый блок возврата
int sum_array(int* input, int length) {
int sum = 0;
for (int i = 0; i < length; ++i) {
if (sum > length * 1024 * 1024)
printf("note: limit exceeded");
sum += input[i];
}
return sum;
}
39
Loop Invariant Code Motion (LICM)
● Вынос инвариантных значений за пределы цикла
● Перенос значений из тела цикла в базовый блок возврата
int sum_array(int* input, int length) {
int sum = 0;
const int len = length * 1024 * 1024;
for (int i = 0; i < length; ++i) {
if (sum > len)
printf("note: limit exceeded");
sum += input[i];
}
return sum;
}
40
Common subexpression elimination (CSE)
● Удаление общих подвыражений
int sum_array(int* input, int length) {
int sum = 0;
int max = 0;
for (int i = 0; i < length; ++i) {
max = (input[i] > max) ? input[i] : max;
sum += input[i];
}
printf("max was %dn", max);
return sum;
}
41
Common subexpression elimination (CSE)
● Удаление общих подвыражений
int sum_array(int* input, int length) {
int sum = 0;
int max = 0;
for (int i = 0; i < length; ++i) {
max = (input[i] > max) ? input[i] : max;
sum += input[i];
}
printf("max was %dn", max);
return sum;
}
42
Common subexpression elimination (CSE)
● Удаление общих подвыражений
int sum_array(int* input, int length) {
int sum = 0;
int max = 0;
for (int i = 0; i < length; ++i) {
const int input_i = input[i];
max = (input_i > max) ? input_i : max;
sum += input_i;
}
printf("max was %dn", max);
return sum;
}
43
Что можно почитать
● blog.llvm.org
● llvm.org/docs
● halt.habrahabr.ru/topics/
● llst.org
Спасибо за внимание!

Tech Talks @NSU: Как приручить дракона: введение в LLVM

  • 1.
    LLVM: Как приручитьдракона? Дмитрий Каш цын, HDSoftии
  • 2.
    Как работает компилятор 1.Прочитать текст программы из файла 2. Записать исполняемый файл
  • 3.
    Чуть детальнее ● Разборлексем и синтаксический анализ ● Построение AST программы ● Преобразования (магия) ● Генерация объектного файла ● Линковка исполняемого файла
  • 4.
  • 5.
    5 Если серьезно ● Развитиеcomputer science не стоит на месте ● Новые идеи — новые языки ● Новые языки — новые компиляторы ● Успех Rust и Go говорит сам за себя
  • 6.
    6 Мой уютный компилятор™ ●Парсинг исходников ● Представление программы? ● Оптимизация всего и вся ● Целевая архитектура, наборы инструкций ● Аллокация и распределение регистров ● Интероперабельность — системные вызовы, FFI, работа с библиотеками ● Генерация исполняемого файла, линковка ● Отладочная информация
  • 7.
    7 Структура команды x86(OMG) http://penberg.blogspot.ru/2010/04/short-introduction-to-x86-instruction.html
  • 8.
    8 Как угодить всем? ●Языки разные. Очень. ● Разное отношение к данным ● Разные модели памяти ● Разные целевые архитектуры ● Разные наборы инструкций
  • 9.
    9 Решение LLVM ● Записьпрограммы в виде, не зависящем от всего вышеперечисленного ● Промежуточное представление — IR код ● Обобщенная система команд
  • 10.
    10 Решение LLVM ● Записьпрограммы в виде, не зависящем от всего вышеперечисленного ● Промежуточное представление — IR код ● Обобщенная система команд Внезапно: LLVM — это не VM o_O
  • 11.
    11 Разбор исходного текста ●LLVM считает, что AST уже есть ● Чтобы его получить, нужно провести лексический и синтаксический анализ ● К счастью, это не надо делать вручную ● На помощь придут – Flex/Bison – ANTLR – И другие
  • 12.
    12 Так видит программучеловек int gcd(int a, int b) { while (b != 0) { if (a > b) a = a − b; else b = b − a; } return a; }
  • 13.
    13 Так видит программукомпилятор condition body else-bodyif-body while variable name: b constant value: 0 compare op: ≠ branch compare op: > assign bin op op: − assign bin op op: − statement sequence return variable name: a variable name: a variable name: a variable name: a variable name: a variable name: b variable name: b variable name: b variable name: b condition
  • 14.
    14 Лексический анализ ● Первичныйразбор текста программы ● Удаление лишнего шума ● Преобразование потока входных символов в последовательность токенов
  • 15.
    15 Лексический анализатор flex ●Позволяет записать правила разбора в текстовом, человеко-читаемом виде ● Регулярные выражения — это сила ● На выходе — токены
  • 16.
    16 Входной файл дляflex (огрызок) whitespace [ rnt]* number [0-9]+ identifier [a-zA-Z]+ {whitespace} { /* ignore whitespaces */ } {number} { sscanf(yytext, "%d", &yylval->value); return TOKEN_NUMBER; } "+" { return PLUS; } "-" { return MINUS; } "(" { return LPAREN; } ")" { return RPAREN; } ";" { return SEMICOLON; } "," { return COMMA; } "=" { return EQ; } "!=" { return NEQ; } ">" { return GTR; } "if" { return IFSYM; } "while" { return WHILESYM; }
  • 17.
    17 Синтаксический анализатор bison ●На вход получает поток токенов от лексера и файл описания грамматики ● На основании правил грамматики производит разбор ● На выходе дает древовидное представление программы с которым может работать компилятор
  • 18.
    18 Входной файл дляbison (огрызок) input: | input line; line: 'n' | exp 'n' { complete("result is %gn", $1); }; exp: TOKEN_NUMBER { $$ = $1; } | exp '+' exp { $$ = $1 + $3; } | exp '-' exp { $$ = $1 - $3; } | exp '/' exp { $$ = $1 / $3; } | exp '*' exp { $$ = $1 * $3; } | '(' exp ')' { $$ = $2; }; Позволяет разбирать арифметические выражения вида (2 + 3 * 4) * 5
  • 19.
    19 IR код ● IntermediateRepresentation ● Запись графа управления в привычном текстовом виде…
  • 20.
    20 IR код ● IntermediateRepresentation ● Запись графа управления в привычном текстовом виде… но с плюшками: – Asm-подобный синтаксис, но с параметризованными функциями – Строгая типизация – Структуры данных, указатели – Const, Volatile модификаторы – Нотация SSA
  • 21.
    21 Нотация SSA ● StaticSingle Assignment ● Переменных — нет о_О ● Все имена встречаются только единожды ● Функциональный стиль описания данных ● Императивный стиль описания операций
  • 22.
    22 Что хорошего вSSA? X ← 1 X ← 2 Y ← X
  • 23.
    23 Что хорошего вSSA? X ← 1 X ← 2 Y ← X X1←1 X2←2 Y1←X2
  • 24.
    24 Что хорошего вSSA? X ← 1 X ← 2 Y ← X X1←1 (RIP) X2←2 Y1←X2
  • 25.
    25 Что хорошего вSSA? X ← 1 X ← 2 Y ← X X1←1 (RIP) X2←2 (RIP) Y1←2
  • 26.
    26 Граф потока управления* ●Control flow graph (CFG) ● Узлы — базовые блоки ● Ребра — инструкции переходов ● Базовый блок — линейный участок кода (a) (c) (b) (d) * https://en.wikipedia.org/wiki/Control_flow_graph
  • 27.
  • 28.
  • 29.
  • 30.
    30 Подсчет суммы массива intsum_array(int* input, int length) { int sum = 0; for (int i = 0; i < length; ++i) sum += input[i]; return sum; }
  • 31.
    31 Листинг IR кода 1; Function Attrs: nounwind readonly 2 define i32 @sum_array(int*, int)(i32* nocapture readonly %input, i32 %length) #0 { 3 %1 = icmp sgt i32 %length, 0 ; а есть вообще что суммировать? 4 br i1 %1, label %.lr.ph, label %._crit_edge 5 ._crit_edge: 6 %sum.0.lcssa = phi i32 [ 0, %0 ], [ %4, %.lr.ph ] 7 ret i32 %sum.0.lcssa ; возврат результата 8 .lr.ph: 9 %i.02 = phi i32 [ %5, %.lr.ph ], [ 0, %0 ] 10 %sum.01 = phi i32 [ %4, %.lr.ph ], [ 0, %0 ] 11 ; вычисление адреса текущего элемента в массиве и его загрузка в регистр 12 %2 = getelementptr inbounds i32, i32* %input, i32 %i.02 13 %3 = load i32, i32* %2, align 4 14 ; аккумулирование суммы и инкремент индекса 15 %4 = add nsw i32 %3, %sum.01 ; новое значение sum 16 %5 = add nuw nsw i32 %i.02, 1 ; новое значение i 17 ; условие выхода 18 %exitcond = icmp eq i32 %5, %length 19 ; проверка условия выхода и переход 20 br i1 %exitcond, label %._crit_edge, label %.lr.ph 21 }
  • 32.
    32 Результат clang -O1-m32 sum_array(int const*, int): mov ecx, dword ptr [esp + 8] xor eax, eax test ecx, ecx jle .LBB0_3 mov edx, dword ptr [esp + 4] .LBB0_2: # %.lr.ph add eax, dword ptr [edx] add edx, 4 dec ecx jne .LBB0_2 .LBB0_3: # %._crit_edge ret
  • 33.
    33 Результат clang -O1(x64) sum_array(int const*, int): xor eax, eax test esi, esi jle .LBB0_2 .LBB0_1: # %.lr.ph add eax, dword ptr [rdi] add rdi, 4 dec esi jne .LBB0_1 .LBB0_2: # %._crit_edge ret
  • 34.
    34 Оптимизации ● Наблюдаемое поведение ●Точки следования ● Эквивалентные преобразования
  • 35.
    35 Основные идеи оптимизаций ●Не делать то, что никому не нужно ● Не делать дважды то, что можно сделать один раз (а лучше не делать вообще) ● Если можно получить тот же результат, но меньшими усилиями — это нужно сделать ● Сокращение издержек на всех уровнях
  • 36.
    36 Виды оптимизаций ● Peepholeоптимизации — буквально «через замочную скважину». Локальные оптимизации в пределах базового блока ● Внутрипроцедурные оптимизации ● Межпроцедурные оптимизации ● Оптимизации во время линковки
  • 37.
    37 Loop Invariant CodeMotion (LICM) ● Вынос инвариантных значений за пределы цикла ● Перенос значений из тела цикла в базовый блок возврата int sum_array(int* input, int length) { int sum = 0; for (int i = 0; i < length; ++i) { if (sum > length * 1024 * 1024) printf("note: limit exceeded"); sum += input[i]; } return sum; }
  • 38.
    38 Loop Invariant CodeMotion (LICM) ● Вынос инвариантных значений за пределы цикла ● Перенос значений из тела цикла в базовый блок возврата int sum_array(int* input, int length) { int sum = 0; for (int i = 0; i < length; ++i) { if (sum > length * 1024 * 1024) printf("note: limit exceeded"); sum += input[i]; } return sum; }
  • 39.
    39 Loop Invariant CodeMotion (LICM) ● Вынос инвариантных значений за пределы цикла ● Перенос значений из тела цикла в базовый блок возврата int sum_array(int* input, int length) { int sum = 0; const int len = length * 1024 * 1024; for (int i = 0; i < length; ++i) { if (sum > len) printf("note: limit exceeded"); sum += input[i]; } return sum; }
  • 40.
    40 Common subexpression elimination(CSE) ● Удаление общих подвыражений int sum_array(int* input, int length) { int sum = 0; int max = 0; for (int i = 0; i < length; ++i) { max = (input[i] > max) ? input[i] : max; sum += input[i]; } printf("max was %dn", max); return sum; }
  • 41.
    41 Common subexpression elimination(CSE) ● Удаление общих подвыражений int sum_array(int* input, int length) { int sum = 0; int max = 0; for (int i = 0; i < length; ++i) { max = (input[i] > max) ? input[i] : max; sum += input[i]; } printf("max was %dn", max); return sum; }
  • 42.
    42 Common subexpression elimination(CSE) ● Удаление общих подвыражений int sum_array(int* input, int length) { int sum = 0; int max = 0; for (int i = 0; i < length; ++i) { const int input_i = input[i]; max = (input_i > max) ? input_i : max; sum += input_i; } printf("max was %dn", max); return sum; }
  • 43.
    43 Что можно почитать ●blog.llvm.org ● llvm.org/docs ● halt.habrahabr.ru/topics/ ● llst.org
  • 44.