Skip to content

ralex2304/Lang

Repository files navigation

Компилятор и язык PPLang

Компиляция программ на своём Тьюринг-полном эзотерическом языке PPLang для собственного SPU и x86-64.

Проведено сравнение эффективности программ. Оптимизации IR ускорили выполнение на 25%. Программа работает на 14% медленнее скомпилированной при GCC -O0 и на 87% медленнее GCC -O3.

Содержание

  1. Процесс компиляции
    1. Frontend
      1. Лексер
      2. Синтаксический парсер
      3. Стандарт AST
      4. Обработка синтаксических ошибок
    2. Middleend
      1. Возможности
      2. Реализация оптимизаций
      3. Предупреждения
    3. Backend
      1. Стандарт IR
    4. IR Backend
      1. Оптимизации IR
      2. Генерация ассемблерного текста
      3. ABI для x86-64
      4. ABI для SPU
      5. Библиотека функций ввода и вывода для x86-64
      6. Парсинг объектного файла библиотеки ввода и вывода
      7. Генерация исполняемого файла ELF64
  2. Возможности и особенности языка
    1. Синтаксические конструкции
  3. Примеры программ
    1. Рекурсивный расчёт факториала
      1. язык Пророка Санбоя
      2. C-подобные ключевые слова
    2. Демонстрация работы областей видимости переменных
      1. C-подобные ключевые слова
  4. Сравнение эффективности скомпилированных программ
    1. Программа на PPLang
    2. Программа на C++
    3. Сравнение сгенерированного ассемблерного кода
    4. Измерение эффективности
      1. Результаты
  5. Заметки о разработке
    1. Развитие проекта
    2. Отладка
      1. Логи
      2. Графические дампы
  6. Зависимости
    1. Зависимости сборки
      1. Библиотеки
        1. Установка
    2. Зависимости компиляции
  7. Использование
    1. Сборка
      1. Release версия
      2. Debug версия
    2. Компиляция программы
      1. Опции компиляции
  8. Итоги
  9. Источники и инструменты
  10. Благодарности

Процесс компиляции

Compilation process

  • Frontend - лексический разбор текста программы и создание абстрактного синтаксического дерева (AST);
  • Middleend - оптимизации и преобразования AST:
    • свёртка констант;
    • удаление мёртвого кода;
    • дифференцирование математических выражений;
  • Backend - генерация линейного архитектурно-независимого промежуточного представления (IR);
  • IR Backend - обработка IR:
    • оптимизации:
      • удаление избыточных парных перемещений данных;
      • свёртка операций со стеком;
    • создание бинарного исполняемого файла программы, генерация ассемблерного листинга.

Note

Компилятор состоит из 4 отдельных программ, описание которых находится ниже. Их названия обусловлены историей развития проекта. Традиционно структура компилятора несколько другая. Например, в LLVM синтаксическое дерево не выходит за пределы frontend, а части компилятора обмениваются IR. В данном проекте основной структурой является AST, поэтому IR появляется лишь на backend.

Frontend

Лексер

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

Note

Полный список ключевых слов. Осторожно, встречается ненормативная лексика.

Синтаксический парсер

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

Условные обозначения:

  • <expr name> := <expr> - определение нового типа выражения
  • '<keyword>' - ключевое слово
  • {<expr>} - скобки для обозначения приоритета
  • !<expr> - выражение не должно встретиться
  • <expr>? - необязательное выражение
  • <expr>* - выражение может не встретиться или встретиться несколько раз
  • <expr1> | <expr2> - выражение 1 или выражение 2

Фрагмент описания грамматики языка в формате, близком к РБНФ:

// 'CH_' - means that function exits without error (gives choice)

Main := {'CMD_SEPARATOR'? {CH_DefFunc | CH_DefVar} 'CMD_SEPARATOR'?}* 'TERMINATOR'

CH_DefFunc := VarName {'VAR' | 'CONST'} 'OPEN_BRACE' FuncArgsDef 'CLOSE_BRACE' 'CMD_SEPARATOR'? CH_Commands

CH_DefVar := VarName {'OPEN_INDEX_BRACE' Expr 'CLOSE_INDEX_BRACE'}? 'VAR' 'CONST'? {'ASSIGNMENT' Expr {'VAR_SEPARATOR' Expr}* }?

CH_Commands := 'OPEN_SCOPE' 'CMD_SEPARATOR'? {Command {'CMD_SEPARATOR' Command}*} 'CMD_SEPARATOR'? 'CLOSE_SCOPE'

Command := {CH_Commands | CH_DefVar | CH_CommandWithArg | CH_ComplexCommand | CH_CommandWithConstArg | SimpleCommand}

// ---------------------------------------------MATHS------------------------------------------------

Expr := {VarName {'OPEN_INDEX_BRACE' Expr 'CLOSE_INDEX_BRACE'}? {{'ASSIGNMENT' !'ASSIGNMENT'} | {{'MATH_ADD' | 'MATH_SUB' | 'MATH_MUL' | 'MATH_DIV'} 'ASSIGNMENT'}} Expr} | MathLvl1

MathLvl1 := MathLvl2 {{{{'ASSIGNMENT' | 'LOGIC_NOT' | 'LOGIC_LOWER' | 'LOGIC_GREATER'} 'ASSIGNMENT'} | {'LOGIC_LOWER' | 'LOGIC_GREATER'}} MathLvl2}*

MathLvl2 := MathLvl3 {{'MATH_ADD' | 'MATH_SUB'} MathLvl3}*

MathLvl3 := MathLvl4 {{'MATH_MUL' | 'MATH_DIV'} MathLvl4}*

MathLvl4 := MathLvl5 {'MATH_POW' MathLvl5}*

MathLvl5 := {{'MATH_SUB' MathLvl5} | {{'OPEN_BRACE' Expr 'CLOSE_BRACE'} | CH_Binary | CH_Unary | Primary}}

Актуальная версия синтаксиса.

Стандарт AST

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

Далее представлена таблица типов узлов дерева:

Num Name Type Description
1 CMD_SEPARATOR LIST Разделитель команд. Имитирует список. Левый потомок - команда, правого или нет, или такой же разделитель
2 VAR_DEFINITION BINARY Определение переменной. Слева лист типа переменная, справа либо ничего, либо выражение
3 CONST_VAR_DEF UNARY Опциональный родитель VAR_DEFINITION и ARRAY_DEFINITION
4 ARRAY_DEFINITION BINARY Определение массива. Слева поддерево: (VAR_SEPARATOR (переменная) (константное выражение - размер массива)). Справа либо ничего, либо список выражений через VAR_SEPARATOR
5 FUNC_DEFINITION BINARY Определение функции. Слева поддерево: (VAR_SEPARATOR (переменная) (поддерево аргументов (список из VAR_SEPARATOR))). Справа список команд через CMD_SEPARATOR
6 ASSIGNMENT BINARY Присваивание. Слева лист переменная, справа выражение
7 ASSIGNMENT_ADD BINARY
8 ASSIGNMENT_SUB BINARY
9 ASSIGNMENT_MUL BINARY
10 ASSIGNMENT_DIV BINARY
11 ARRAY_ELEM BINARY Элемент массива. Слева лист переменная - имя массива, справа выражение - индекс элемента
15 VAR_SEPARATOR LIST Имитатор списка для аргументов функции и т.п.
16 FUNC_CALL BINARY Вызов функции. Слева лист переменная, справа список выражений через VAR_SEPARATOR
17 RETURN UNARY Возврат из функции. Слева ничего, справа выражение
20 MATH_ADD BINARY Сложение
21 MATH_SUB BINARY Вычитание
22 MATH_MUL BINARY Умножение
23 MATH_DIV BINARY Деление
24 MATH_SQRT UNARY Корень
25 MATH_SIN UNARY Синус
26 MATH_COS UNARY Косинус
27 MATH_NEGATIVE UNARY Унарный минус
28 MATH_DIFF BINARY Оператор дифференцирования. Слева выражение, справа лист-переменная с номером переменной, по которой дифференцируем
40 LOGIC_GREAT BINARY >
41 LOGIC_LOWER BINARY <
42 LOGIC_NOT_EQUAL BINARY !=
43 LOGIC_EQUAL BINARY ==
44 LOGIC_GREAT_EQ BINARY >=
45 LOGIC_LOWER_EQ BINARY <=
50 PREFIX_ADD BINARY ++x
1) Слева обязательно переменная, справа, либо следующий препост-оператор, либо ничего, либо переменная (последние два варианта означают одно и то же). В таком списке операторов все переменные должны иметь один номер. Такое дублирование сделано для оптимального чтения на бекенде;
2) Сначала только префиксные операторы, потом только постфиксные. То есть префиксный не может быть потомком постфиксного.

Аналогично для всех препост-операторов
51 PREFIX_SUB BINARY --x
52 POSTFIX_ADD BINARY x++
53 POSTFIX_SUB BINARY x--
60 WHILE BINARY while. Слева вычисляемое выражение, справа либо список команд, либо ELSE
61 DO_WHILE BINARY do {} while (). Аналогично, ELSE нельзя
63 IF BINARY Аналогично
64 DO_IF BINARY do {} if () - условный блок с пост-условием. В случае невыполнения программа завершается с ошибкой (вероятен segmentation fault или другое неопределённое поведение) :-D
66 ELSE BINARY Слева список команд, если выполняется основная ветвь, справа если else ветвь
67 BREAK LEAF break
68 CONTINUE LEAF continue
69 NEW_SCOPE UNARY Новая область видимости переменных. Слева ничего, справа список команд
70 IN LEAF Ввод числа пользователем
71 OUT UNARY Вывод числа для пользователя. Слева ничего, справа выражение
72 SHOW LEAF команда ассемблера SPU shw
73 SET_FPS UNARY команда ассемблера SPU fps. Слева ничего, справа выражение, которое можно вычислить во время компиляции (константное)

Узлы могут быть 3 типов:

  • 1 - оператор. Значение - номер оператора из таблицы
  • 2 - число. Значение - число
  • 3 - переменная. Значение - номер переменной

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

Полное описание, формат текстового файла для передачи дерева и прочая информация в репозитории стандарта.

Обработка синтаксических ошибок

При выявлении синтаксической ошибки выводится gcc-подобное сообщение.

Middleend

Данная программа использует наработки из проекта математического дифференциатора.

Возможности

  • свёртка константных выражений;
  • упрощение математических выражений:
    • удаление нейтральных элементов (x + 0, x * 1);
    • свёртка выражений независимым результатом (x * 0);
    • удаление парных постфиксных и префиксных операторов (x++--);
  • удаление мёртвого кода;
  • математическое дифференцирование по произвольной переменной.

Реализация оптимизаций

Для проведения оптимизаций введена дополнительная классификация типов узлов-операторов дерева:

# Name Description
0 NO_MATH Не является математическим выражением
1 MATH Математический оператор, оба потомка являются математическими выражениями
2 MATH_L Не математический оператор, левый потомок - математическое выражение
3 MATH_R Не математический оператор, правый потомок - математическое выражение
4 MATH_L_R Не математический оператор, оба потомка - математические выражения

Предупреждения

Например, выявляются константные условные выражения и выдаются предупреждения о недостижимости участка кода:

Backend

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

Стандарт IR

Аналогично стандарту AST описан и формат IR. Подробная информация в том же репозитории.

Любая программа представляет собой последовательность блоков IR. Для любого блока можно задать 2 входных и одно выходное значение (так называемое трёхадресное представление).

Далее представлена таблица типов блоков IR:

Num Name src[0] src[1] dest subType Description
0 NONE Empty block. May be used for jump destination
1 START global vars number Program beginning, initialization and entry point start. Must be the first block
2 END Entry point end
3 BEGIN_FUNC_DEF local vars number Function definition beginning
4 END_FUNC_DEF Function definition end
5 CALL_FUNC local vars number func block index Function call
6 RET Return from function
7 COUNT_ARR_ELEM_ADDR_CONST offset Count address of array element
8 ARR_ELEM_ADDR_ADD_INDEX index source global or local Add value to address of array element
9 MOV source special data destination Mov value from src[0] to dest (stack, memory, register)
10 SWAP operand 1 operand 2 Swap 2 values from src[0] and src[1]
11 STORE_CMP_RES operand 1 operand 2 result CmpType Write bool result of comparison
12 SET_FLAGS_CMP_WITH_ZERO operand Compare with zero and set comparison flags
13 MATH_OPER operand 1 operand 2 result MathOper Math operation
14 JUMP dest block index JmpType Conditional or unconditional jump
15 READ_DOUBLE value Read double precision floating point number from user
16 PRINT_DOUBLE value Print double precision floating point number
17 SET_FPS value SPU: asm fps <value> - set max fps count for video mode
18 SHOW_VIDEO_FRAME SPU: asm shw - show image frame in video mode

Типы входных и выходных значений:

# Имя Описание
1 CONST Константное число с плавающей точкой
2 INT_CONST Константное целое число
3 LOCAL_VAR Локальная переменная
4 GLOBAL_VAR Глобальная переменная
5 ARG_VAR Аргумент функции (для передачи при вызове)
6 ARR_VAR Элемент массива
7 STK Стек
8 REG Регистр
9 ADDR Индекс другого блока IR (для вызовов функций и прыжков)

Внутри программы последовательность блоков хранится в двусвязном списке (List). Это позволяет удобно удалять и вставлять блоки при дальнейших оптимизациях, так как тогда физические индексы блоков не меняются.

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

IR Backend

Здесь оптимизируется последовательность блоков IR и генерируется ассемблерный текст или исполняемый файл для двух архитектур: x86-64 и SPU.

Оптимизации IR

Из-за стековой архитектуры вычислений часто возникают парные бессмысленные операции push и pop, а также лишние промежуточные перемещения данных.

Делается один проход оптимизации. Каждая операция push помещается в стек. При встрече pop происходит проверка: есть ли какие-то операции между данным pop и последним push. Если нет, то эти две операции либо уничтожаются, либо заменяются на одну.

Пример соптимизированных операций:

Сравнение IR с оптимизациями и без

Прирост быстродействия программы на x86-64 составил порядка 25%. Подробное исследование ниже.

Генерация ассемблерного текста

Ассемблерный текст очень удобен для отладки эмиттера и ABI. Поэтому каждый блок IR отмечен меткой, которая содержит его индекс.

Из линейного IR достаточно тривиально реализуется генерация ассемблерного текста программы. В IR все прыжки и вызовы функций ссылаются на другие блоки IR по их индексам. Так удаётся переложить проблему расчёта адресов на ассемблер. В дальнейшем при генерации бинарного исполняемого файла будут реализованы привязки (fixups).

ABI для x86-64

Назначение регистров:

  • xmm0 - возвращаемое значение функции
  • rbp - начало стекового фрейма локальных переменных функции
  • xmm1-xmm3, rax, rdx - вычисления
  • rcx - адрес элемента массива (используется для расчёта и индексации)

Соглашение о вызове функций:

  • вызываемая функция должна сохранить rsp и rbp;
  • передача аргументов через стек:
    • внутри функции формальные аргументы считаются первыми локальными переменными;
    • при вызове функции происходит сохранение адреса возврата (call) и внутри самой функции сохраняется rbp (enter), поэтому первый фактический аргумент кладётся по адресу [rsp - 16], второй - [rsp - 16 - 8] и так далее.

ABI для SPU

Назначение регистров:

  • rax - возвращаемое значение функции
  • rbx - начало стекового фрейма локальных переменных функции
  • rcx - адрес элемента массива (используется для расчёта и индексации)
  • rdx, rex - вычисления

Соглашение о вызове функций:

  • вызываемая функция обязана сохранять rbx и стек вычислений;
  • передача аргументов через виртуальный стек в оперативной памяти:
    • внутри функции формальные аргументы считаются первыми локальными переменными;
    • первый фактический аргумент кладётся по адресу [rbx + x], второй [rbx + x + 1] и так далее, где x - количество локальных переменных в вызывающей функции.

Библиотека функций ввода и вывода для x86-64

Библиотека написана на ассемблере и содержит наработки из проекта AsmPrintf. Текст библиотеки.

Она содержит 2 функции:

  • doubleio_in - чтение числа с плавающей точкой в xmm0 из stdin;
  • doubleio_out - печать числа с плавающей точкой с фиксированной точностью (6 знаков после запятой) из xmm0 в stdout.

При генерации ассемблерного текста библиотека подключается при помощи директивы %include ассемблера nasm. Для бинарного ELF64 происходит парсинг объектного файла библиотеки, сгенерированного nasm.

Парсинг объектного файла библиотеки ввода и вывода

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

Для обработки была написана небольшая библиотека. При чтении проверяются поля Ehdr. По названиям и типам в таблице сегментных заголовков ищутся нужные поля для .rodata, .data, .text, .rela.text, .symtab, .strtab. При помощи найденных заголовков производятся нужные операции и извлекается необходимая информация, которая будет использована при генерации бинарного исполняемого файла.

Генерация исполняемого файла ELF64

Сгенерированный файл имеет следующую структуру:

  1. Заголовок Ehdr;
  2. 4 заголовка Phdr для сегментов Ehdr, .rodata, .data и .text;
  3. Сами сегменты:
    1. .rodata - константы
    2. .data - глобальные переменные
    3. .text - сама программа

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

При генерации программы используются команды jmp и call с относительными 32-битными смещениями. Если адрес прыжка ещё неизвестен (прыжок вперёд), то в структуре блока IR, на который должен произойти прыжок, инициализируется динамический массив. В него помещается смещение от начала бинарного файла, по которому нужно записать неизвестное значение. Соответственно, когда при обходе станет известен адрес этого блока IR, по всем смещениям из этого массива будут вписаны верные значения.

Для упрощения отладки и верификации генерации бинарных команд был реализован DSL. Так же были написаны функции для автоматической генерации полей ModRM и SIB.

Проверка сгенерированных программ проводилась при помощи утилит readelf, objdump, дизассемблера ghidra и дебагера ghidra.

Возможности и особенности языка

В целом синтаксис языка похож на C.

Символ новой строки является разделителем команд.

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

Объявление функции совпадает с её определением. Поэтому возможен вызов функции, объявление которой расположено ниже.

Исполнение программы начинается из главной функции (она должна иметь название main, остров_в_океане или министерство).

В языке есть только 1 тип переменных - числа с плавающей точкой двойной точности (64 битные IEEE 754, double из C)

Синтаксические конструкции

<abc> означает, что на этом месте должны быть конструкция, соответствующая описанию.

abc курсив означает необязательную конструкцию

--- после черты находятся примеры использования

  • Объявление переменной:
<имя> var const = <выражение>
---
x var
---
y var = 123
---
z var const = 123 + 456
  • Объявление массива:
<имя> var const [<размер>] = <значение 1>, <значение 2>, ...
---
x var [3]
---
y var [] = 1, 2, 3
---
z var const [5] = 1 + 2, 3 + 4

Значений должно быть не более размера. Если размер не указан, то он соответствует количеству значений.

  • Обращение к элементу массива:
<имя>[<индекс>]
---
x[3]
---
y[1 + 2]
  • Объявление функции:
<имя> var (<аргументы через запятую (объявления переменных)>) {
    <тело функции>
}
---
func1 var () {
    x = 1 + 2
}
---
func2 var (x var, y var const) {
    x += y
}
  • Возврат значения из функции:
return <выражение>
---
return x + 1
  • Вызов функции:
<имя>(<выражения через запятую>)
---
func1()
---
x = func2(1, 2)

Возвращаемое функцией значение может быть использовано в любом выражении

  • Математические выражения:

    Приоритет операций совпадает с таковым в языке C.

    a и b - выражения; x - имя переменной.

    • Элементарные операции:
    a + b
    a - b
    a * b
    a / b
    -a
    a ^ b
    log(a)
    sqrt(a)
    sin(a)
    cos(a)
    
    • Дифференцирование:

    x - имя переменной параметра дифференцирования.

    diff(a, x)
    
    • Присваивания:
    x =  a
    x += a
    x -= a
    x *= a
    x /= a
    
    • Префиксные и постфиксные операции:
    x++
    x--
    ++x
    --x
    

    Такие операции работают аналогично таковым в языке C. Для ++x, --x сначала происходит операция, затем значение возвращается; для x++, x-- наоборот.

    Могут использованы конструкции типа ----x++++ (произойдет вычитание 2, возврат значения в выражение, прибавление 2).

    • Операторы сравнения:
    a < b
    a > b
    a <= b
    a >= b
    a == b
    a != b
    

    Все сравнения производятся через дополнительное сравнение модуля разности выражений с EPSILON = 1e-6. Результат - это числа 0.0 и 1.0.

  • Условное выражение:


if (<выражение>) {
    <блок команд>
} else {
    <блок команд>
}
---
if (x + 3) {
    func1()
    x += 1
}
---
if (x <= 3)
    x -= 3
---
if (x == 0)
    x = 1
else
    x = 123

Также возможна конструкция else if (<выражение>). Аналогично языку C фигурные скобки можно опустить, если блок содержит одну команду.

  • Цикл:

while (<выражение>) {
    <блок команд>
} else {
    <блок команд>
}
---
while (x--) {
    y = x + 1
    func1(z = y)
}
---
while (x++) {
    if (x + y < -3)
        break
} else
    func3(x)

Блок else выполнится в случае, если выход из цикла произойдёт из-за невыполнения выражения, а не команды break.

  • Цикл с пост-условием:

do {
    <блок команд>
} while (<условие>)
---
do
    x = 3
while (y++ < 3)
---
do {
    x *= 2
    y -= 1
} while (x == --z)
  • Выход из цикла:
break
  • Переход на следующую итерацию цикла:
continue
  • Печать числа в stdout:
out(<выражение>)
---
out(x + 3)
---
out(y = 2)
  • Ввод числа из stdin:
in
---
x = in
---
y = in + 3
  • Комментарии:
// Однострочный комментарий

/*
Многострочный комментарий
*/

Примеры программ

Note

Примеры программ в папке /Programs. Осторожно, встречается ненормативная лексика.

В папке есть небольшой readme

Рекурсивный расчёт факториала

Обе программы ниже распознаются как одна и та же программа. Синтаксическое дерево (AST) данной программы:

factorial AST

язык Пророка Санбоя

остров_в_океане мой () {

    серенада(факториал(слушай))

    свергаю кумира 0
}

факториал мой (счётчик мой) {

    что это такое (счётчик > 1)
        свергаю кумира факториал(счётчик - 1) * счётчик

    свергаю кумира 1
}

C-подобные ключевые слова

main var () {

    out(fact(in))

    return 0
}

fact var (i var) {

    if (i > 1)
        return fact(i - 1) * i

    return 1
}

Демонстрация работы областей видимости переменных

C-подобные ключевые слова

main var() {

    x var = 5

    if (1) {
        out(x) // 5

        x var = 10

        out(x) // 10

        x += 10

        out(x) // 20
    }

    out(x) // 5

    return 0
}

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

Сравнение эффективности скомпилированных программ

Для сравнения эффективности была написана программа с функцией рекурсивного расчёта факториала натурального числа. Функция вызывается 10^7 раз с аргументом 20 (расчёт 20!).

Программа на PPLang

fact var (i var) {

    if (i > 1)
        return fact(i - 1) * i

    return 1
}

main var () {

    N var = 1e7

    while (N-- > 0)
        fact(20)

    return 0
}

Программа на C++

Для сравнения с компилятором GCC была написана аналогичная программа на C++:

#include <math.h>

const double EPS = 1e-6;

double fact(const double i) {

    if (i > 1 && fabs(i - 1) >= EPS)
        return fact(i - 1) * i;

    return 1;
}

int main() {

    double N = 1e7;

    while (fabs(N) >= EPS  && N-- > 0)
        fact(20);

    return 0;
}

Все числа типа double и сравнения через EPS = 1e-6, так как именно такие операции подставляет компилятор PPLang.

Сравнение сгенерированного ассемблерного кода

На изображении слева представлено лишь начало (сравнение числа) функции расчёта факториала, сгенерированное компилятором. Справа полный текст той же функции, полученный при помощи GCC.

Большое различие в объёме кода обусловлено отсутствием оптимизацией, а также стековой архитектурой вычислений, использованной в данном проекте.

Измерение эффективности

Параметры тестовой машины:

  • Компилятор: g++ (GCC) 13.2.1 20230801
  • Процессор: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz SkyLake
  • ОС: Arch Linux, Kernel: 6.6.22-1-lts (64-bit)
  • Профилировщик: perf 6.7-2
  • Графический интерфейс для обработки данных: hotspot 1.4.80

Результаты

Компилятор Быстродействие
Cycles * 10^7 % от первого % от предыдущего
x86-64 495 +- 3 100%
x86-64 без оптимизаций IR 661.6 +- 1.9 133% 133%
SPU 12677 +- 30 2556% 1916%
GCC -O0 428 +- 3 86% 3%
GCC -O2 68.2 +- 0.4 14% 16%
GCC -O3 66.5 +- 0.1 13% 97%

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

Ожидаемо, компилятор PPLang генерирует менее эффективный код в сравнении с GCC.

Эмуляция процессора оказалась в 25 раз медленнее прямого исполнения.

Заметки о разработке

Развитие проекта

Проект создавался в конце первого семестра обучения как компилятор собственного языка для разработанного ранее программного стекового процессора (SPU). Компилятор генерировал только текстовый ассемблерный текст.

Во втором семестре была поставлена задача трансляции для x86-64. Сначала в существующий backend было добавлено создание ассемблерного текста для nasm. Выбор между двумя архитектурами был реализован через таблицу указателей на функции, что позволило очень легко интегрировать нововведения в проект.

Далее было принято решение о создании промежуточного представления для проведения некоторых оптимизаций для x86-64. Они стали возможны, так как SPU осуществляет все математические операции через стек. Соответственно некоторые действия можно свернуть или удалить.

С этого момента backend генерирует архитектурно-независимое линейное промежуточное представление программы (IR). Был создан IR backend. В нём реализованы оптимизации промежуточного представления и генерация кода для обеих архитектур. В том числе добавлено создание бинарного исполняемого файла elf64.

Отладка

Для облегчения отладки были созданы несколько инструментов логирования и графические дампы некоторых структур данных:

  • Stack - стек. Используется в backend для реализации областей видимости переменных и в IR backend для оптимизаций.
  • Tree - бинарное дерево. Используется для работы с AST.
  • List - двусвязный список. Используется для работы с IR.

Логи

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

<pre>
<font color="red">!!! POISON_VAL_FOUND: There is poison value in list
</font></pre>
<pre>
<font color="red">!!! DAMAGED_PATH: List is damaged. Invalid path
</font></pre>
<pre>
    list_dump() called from ../shared/List/list.cpp:51 list_dtor
    r[0x7a29e7400020] initialized in ../shared/ir_reader/ir_reader.cpp:52 read_ir_process_
    {
    real capacity  = 33
    size           = 2
    head           = 1
    tail           = 0
    free_head      = 1
    is_linear      = true
        {
          i | prev | next | elem       | src[0]           | src[1]           | dest             | subtype
          0 |    0 |    1 | type =  -1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |
          1 |    0 |    2 | type =   1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |
          2 |    1 |    3 | type =  -1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |
          3 |   -1 |    4 | type =  -1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |
          4 |   -1 |    5 | type =  -1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |
          5 |   -1 |    6 | type =  -1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |

          ...

         32 |   -1 |    0 | type =  -1 | {NONE, -}        | {NONE, -}        | {NONE, -}        |
        }
    Ordered elements: type = 1 type = -1 type = -1
    Physical indexes: 1 2 3
    }
</pre>
<img src="../../log/08-05-2024_02-05-45/0.svg">

Графические дампы

Пример графического дампа AST. Картинка создана автоматически при помощи graphviz.

Дамп AST

Зависимости

Зависимости сборки

  1. GNU make - система сборки
  2. clang - компилятор - можно заменить на gcc, изменив четыре Makefile
  3. bear - необязательно (make build nobear=1) - утилита для создания файла compile_commands.json (нужен для языкового сервера clangd)

Библиотеки

  1. Tree и/или TreeDebug - релизы - библиотека для работы с бинарными деревьями
Установка

Заменить символические ссылки в директории ./lib на папки с файлами .h и .a. Скачать их можно в разделе Releases соответствующих библиотек

Зависимости компиляции

  1. iconv - используется скриптом compiler.sh для отображения русских символов в консоли
  2. graphviz dot - необязательно - графический дамп внутренних структур программ при ошибках (только в Debug версии)

Использование

Сборка

make build <release=1> <nobear=1>

Release версия

  • опция компиляции -O2.
make build release=1

Debug версия

  • верификаторы структур данных;
  • логирование внутренних ошибок;
  • sanitizer;
  • опция компиляции -Og;
  • assert.
make build

Компиляция программы

Note

По умолчанию выбрана архитектура x86-64

./compiler.sh <file>

Опции компиляции

Опция Описание
--help Вывести опции компиляции
-o <file> Задать имя выходного файла
-m={spu|x86_64} Выбрать архитектуру
-l=<file> Задать имя объектного файла библиотеки ввода/вывода
-S Включить генерацию текстового ассемблерного листинга

Итоги

Созданный язык и компилятор позволили изучить и реализовать существующие технологии, использованные в промышленных компиляторах и программах. Скомпилированные программы уступают в эффективности на 14% в сравнении с GCC -O0 и на 87% с GCC -O3. Оптимизации IR позволили ускорить программы на 25%.

Источники и инструменты

  1. Computer Systems: A Programmer's Perspective 3rd Edition by Randal Bryant, David O'Hallaron
  2. Intel 64 and IA-32 Architectures Software Developer’s Manual March 2024
  3. Compiler explorer - godbolt.com
  4. Perf - perf.wiki.kernel.org
  5. x86 and amd64 instruction reference - felixcloutier.com/x86
  6. Online Assembler and Disassembler - https://shell-storm.org
  7. elf.h - github.com
  8. ELF manpage - manpages.debian.org
  9. GNU Binutils - gnu.org
  10. Ghidra Software Reverse Engineering Framework - github.com
  11. Jupyter Notebook - jupyter.org
  12. Python Matplotlib - matplotlib.org

Благодарности

Спасибо за замечательный курс, за то, что делились опытом, и за ваше безграничное терпение! ❤️ Meow ❤️

About

Programming language and compiler

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors