- •Об авторе
- •О группе редакторов
- •Предисловие
- •Введение
- •Как использовать эту книгу
- •Загрузка исходного кода CPython
- •Что в исходном коде?
- •Настройка среды разработки
- •IDE или редактор?
- •Настройка Visual Studio
- •Настройка Visual Studio Code
- •Настройка Vim
- •Выводы
- •Компиляция CPython
- •Компиляция CPython на macOS
- •Компиляция CPython на Linux
- •Установка специализированной версии
- •Знакомство с Make
- •Make-цели CPython
- •Компиляция CPython на Windows
- •Профильная оптимизация
- •Выводы
- •Грамматика и язык Python
- •Спецификация языка Python
- •Генератор парсеров
- •Повторное генерирование грамматики
- •Выводы
- •Конфигурация и ввод
- •Конфигурация состояния
- •Структура данных конфигурации среды выполнения
- •Конфигурация сборки
- •Сборка модуля из входных данных
- •Выводы
- •Генерирование конкретного синтаксического дерева
- •Парсер/токенизатор CPython
- •Абстрактные синтаксические деревья
- •Важные термины
- •Пример: добавление оператора «почти равно»
- •Выводы
- •Компилятор
- •Исходные файлы
- •Важные термины
- •Создание экземпляра компилятора
- •Флаги будущей функциональности и флаги компилятора
- •Таблицы символических имен
- •Основная компиляция
- •Ассемблер
- •Создание объекта кода
- •Использование Instaviz для вывода объекта кода
- •Пример: реализация оператора «почти равно»
- •Выводы
- •Цикл вычисления
- •Исходные файлы
- •Важные термины
- •Построение состояния потока
- •Построение объектов кадров
- •Выполнение кадра
- •Стек значений
- •Пример: добавление элемента в список
- •Выводы
- •Управление памятью
- •Выделение памяти в C
- •Проектирование системы управления памятью Python
- •Аллокаторы памяти CPython
- •Область выделения объектной памяти и PyMem
- •Область выделения сырой памяти
- •Нестандартные области выделения памяти
- •Санитайзеры выделенной памяти
- •Арена памяти PyArena
- •Подсчет ссылок
- •Сборка мусора
- •Выводы
- •Параллелизм и конкурентность
- •Модели параллелизма и конкурентности
- •Структура процесса
- •Многопроцессорный параллелизм
- •Многопоточность
- •Асинхронное программирование
- •Генераторы
- •Сопрограммы
- •Асинхронные генераторы
- •Субинтерпретаторы
- •Выводы
- •Объекты и типы
- •Примеры этой главы
- •Встроенные типы
- •Типы объектов
- •Тип type
- •Типы bool и long
- •Тип строки Юникода
- •Словари
- •Выводы
- •Стандартная библиотека
- •Модули Python
- •Модули Python и C
- •Набор тестов
- •Запуск набора тестов в Windows
- •Запуск набора тестов в Linux или macOS
- •Флаги тестирования
- •Запуск конкретных тестов
- •Модули тестирования
- •Вспомогательные средства тестирования
- •Выводы
- •Отладка
- •Обработчик сбоев
- •Компиляция поддержки отладки
- •LLDB для macOS
- •Отладчик Visual Studio
- •Отладчик CLion
- •Выводы
- •Бенчмаркинг, профилирование и трассировка
- •Использование timeit для микробенчмарка
- •Использование набора тестов производительности Python
- •Профилирование кода Python с использованием cProfile
- •Выводы
- •Что дальше?
- •Создание расширений C для CPython
- •Улучшение приложений Python
- •Участие в проекте CPython
- •Дальнейшее обучение
- •Препроцессор C
- •Базовый синтаксис C
- •Выводы
- •Благодарности
128 Компилятор
АССЕМБЛЕР
После завершения этих стадий компиляции у компилятора имеется список блоков (frame blocks), каждый из которых содержит список инструкций и указатель на следующий блок. Ассемблер выполняет поиск в глубину (DFS, Depth-First Seach) по базовым блокам и объединяет инструкции в одну последовательность байт-кода.
Структура данных ассемблера
Структура данных ассемблера assembler объявляется в файле Python compile.c и содержит следующие поля.
ПОЛЕ |
ТИП |
НАЗНАЧЕНИЕ |
a_bytecode |
PyObject * (str) |
Строка, содержащая байт-код |
a_lineno |
int |
Последнее значение lineno сгенерированной |
|
|
инструкции |
a_lineno_off |
int |
Смещение последней lineno в байт-коде |
a_lnotab |
PyObject * (str) |
Строка, содержащая lnotab |
a_lnotab_off |
int |
Смещение в lnotab |
a_nblocks |
int |
Количество достижимых блоков1 |
a_offset |
int |
Смещение в байт-коде |
a_postorder |
basicblock ** |
Список блоков в обратном порядке DFS |
Алгоритм поиска в глубину
Ассемблер использует поиск в глубину (DFS) для обхода узлов в графе базовых блоков. Алгоритм DFS не является специфическим для CPython, но часто используется при обходе графов.
Если CST и AST являются структурами деревьев, то состояние компилятора является структурой графа, в которой узлам соответствуют базовые блоки, содержащие инструкции.
1 Блоки, для которых существуют пути между парами вершин. — Примеч. ред.
Книги для программистов: https://t.me/booksforits
Ассемблер 129
Базовые блоки связываются двумя графами. В одном графе используется обратный порядок создания, основанный на свойстве b_list каждого блока. Серия базовых блоков с именами от A до O будет выглядеть так:
A B C D E
F G H I J
K L M N O
Граф, созданный на b_list, используется для последовательного обхода всех блоков в единице компиляции.
Второй граф использует свойство b_next каждого блока. Этот список представляет поток управления. Вершины графа создаются вызовами compiler_ use_next_block(c, next), где next — следующий блок, для которого будет создана вершина, связанная ребром с текущим блоком (c->u->u_curblock).
Граф узла цикла for может выглядеть примерно так:
|
|
|
|
End |
|
|
|
FOR_LOOP |
|
|
|
A |
B |
C |
Start |
D |
E |
|
|||||
|
Body |
OrElse |
|
Cleanup |
|
|
|
|
|
|
|
F |
G |
H |
|
I |
J |
K |
L |
M |
|
N |
O |
Используются оба графа — как последовательный, так и потока управления, — но реализация DFS использует граф потока управления.
Книги для программистов: https://t.me/booksforits
130 Компилятор
API ассемблера на C
API ассемблера содержит точку входа assemble(), которая решает следующие задачи:
zz Вычисляет количество блоков для выделения памяти.
zz Гарантирует, что каждый блок, выходящий за границу, возвращает None.
zz Выполняет разрешение всех смещений команд перехода, помеченных как относительные.
zz Вызывает dfs() для выполнения обхода блоков в глубину. zz Генерирует все инструкции для компилятора.
zz Вызывает makecode() с состоянием компилятора для генерирования
PyCodeObject.
Python compile.c, строка 6010
static PyCodeObject *
assemble(struct compiler *c, int addNone)
{
...
if (!c->u->u_curblock->b_return) { NEXT_BLOCK(c);
if (addNone)
ADDOP_LOAD_CONST(c, Py_None); ADDOP(c, RETURN_VALUE);
}
...
dfs(c, entryblock, &a, nblocks);
/* Байт-код не может изменяться после вычисления смещений */ assemble_jump_offsets(&a, c);
/* Сгенерировать код в обратном порядке */ for (i = a.a_nblocks - 1; i >= 0; i--) {
b = a.a_postorder[i];
for (j = 0; j < b->b_iused; j++)
if (!assemble_emit(&a, &b->b_instr[j])) goto error;
}
...
co = makecode(c, &a); error:
assemble_free(&a); return co;
}
Книги для программистов: https://t.me/booksforits
Создание объекта кода 131
Поиск в глубину
Поиск в глубину выполняется функцией dfs() в Python compile.c. Эта функция переходит по указателям b_next в каждом из блоков, помечает их как просмотренные, изменяя значение b_seen, после чего добавляет их в список a_postorder ассемблера в обратном порядке.
Функция в цикле перебирает обратный список ассемблера и для каждого блока, если он содержит операцию перехода, рекурсивно вызывает dfs() для этого перехода:
Python compile.c, строка 5441
static void
dfs(struct compiler *c, basicblock *b, struct assembler *a, int end)
{
int i, j;
/* Исключение рекурсии для обычного потока управления.
Так как количество блоков ограничено, неиспользуемое пространство в a_postorder (от a_nblocks до end) может использоваться
как стек для еще не упорядоченных блоков */ for (j = end; b && !b->b_seen; b = b->b_next) {
b->b_seen = 1; assert(a->a_nblocks < j); a->a_postorder[--j] = b;
}
while (j < end) {
b = a->a_postorder[j++];
for (i = 0; i < b->b_iused; i++) { struct instr *instr = &b->b_instr[i]; if (instr->i_jrel || instr->i_jabs)
dfs(c, instr->i_target, a, j);
}
assert(a->a_nblocks < j); a->a_postorder[a->a_nblocks++] = b;
}
}
После того как ассемблер построит граф в CFG при помощи поиска в глубину, можно переходить к созданию объекта кода.
СОЗДАНИЕ ОБЪЕКТА КОДА
Задача функции makecode() — перебрать состояние компилятора и некоторых свойств ассемблера и поместить их в PyCodeObject вызовом PyCode_New().
Книги для программистов: https://t.me/booksforits
132 Компилятор
Имена переменных и констант помещаются в объект кода как свойства:
Python compile.c, строка 5893
static PyCodeObject *
makecode(struct compiler *c, struct assembler *a)
{
...
consts = consts_dict_keys_inorder(c->u->u_consts); names = dict_keys_inorder(c->u->u_names, 0); varnames = dict_keys_inorder(c->u->u_varnames, 0);
...
cellvars = dict_keys_inorder(c->u->u_cellvars, 0);
...
freevars = dict_keys_inorder(c->u->u_freevars,
PyTuple_GET_SIZE(cellvars));
...
flags = compute_code_flags(c); if (flags < 0)
goto error;
bytecode = PyCode_Optimize(a->a_bytecode, consts, names, a->a_lnotab);
...
co = PyCode_NewWithPosOnlyArgs( posonlyargcount+posorkeywordargcount, posonlyargcount, kwonlyargcount, nlocals_int, maxdepth, flags, bytecode, consts, names, varnames, freevars, cellvars, c->c_filename, c->u->u_name, c->u->u_firstlineno, a->a_lnotab);
...
return co;
}
Возможно, вы заметите, что байт-код передается PyCode_Optimize() перед отправкой PyCode_NewWithPosOnlyArgs(). Эта функция является частью процесса оптимизации байт-кода в Python peephole.c.
Оптимизатор глазка перебирает инструкции байт-кода и в некоторых случаях заменяет их другими инструкциями. Например, существует оптимизатор, который удаляет все недостижимые инструкции после команды return.
Книги для программистов: https://t.me/booksforits