Perl изнутри

Автор: Ханов Артур, "Перлклуб" УрГУ

Здесь представлен текст доклада, представленного на воркшопе "Perlburg", который прошел 20 февраля 2010 года.


Perl можно рассматривать как обычную виртуальную машину. Когда мы пишем для нее программу, то мы даже не задумываемся о том, как происходит ее разбор и исполнение. Но Perl дает нам очень серьезную абстракцию над теми типами данных и операциями над ними, которые есть в языке, на котором он написан - Си. Если узнать чуть подробнее об этом, то можно писать более быстрые и надежные программы. В начале я расскажу про общие механизмы работы - этапы исполнения, потом рассмотрим, как устроена память машины.

Начнем с того, как самому(-ой) заняться разбором исходных кодов Perl. Прежде всего нужно скачать какую-нибудь версию с http://www.cpan.org/src/README.html. Я использовал версию 5.10.1, хотя к тому времени были уже исходные коды для 5.11. После этого нужно научиться компилировать Perl под свою ОС. Я компилировал под Windows XP через консоль VS. Хотя основные рабочие файлы общие для всех. В документациях perlguts, perlapi, perlcall описаны методы работы с функциями Perl через Си, однако они не раскрывают нам никаких секретов - они просто позволяют работать с машиной в обход интерпретатора. Поэтому в них нет необходимой нам информации в достаточном объеме. В коде даны отличные комментарии, которые в общих чертах позволяют разобраться с тем, что написано.

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

  1. Разбор argv, argc, env
  2. perl_alloc - выделение памяти под интерпретатор
  3. perl_construct - создание структур, связанных с разбором программы
  4. perl_parse - разбор программы, перевод в байткод
  5. perl_run - запуск и исполнение байт-кода
  6. perl_destruct
  7. perl_free

Первое, что было решено рассмотреть подробнее, - это perl_alloc. Оказалось, что под интерпретатор выделяется 824 байта в специальной области памяти, контролируемой объектом CPerlHost. Этот объект содержит 3 объекта Vmem, который служит для операций с памятью. Первая мысль, которая возникла при прочтении - этот объект и используется для операций со всей памятью машины. Но это оказалось не так. Он нужен лишь для хранения внутренних структур машины, а динамические структуры, возникающие при исполнении, управляются другим менеджером памяти в malloc.h. Как устроен Vmem.h? В нем реализовано несколько алгоритмов для управления кучей из главы 2.5 1 тома Кнута. Все эти режимы выбираются с помощью флагов компиляции.

По-умолчанию включен флаг _USE_MSVCRT_MEM_ALLOC - использовать функции malloc, realloc и free из msvcrt.dll. Если же стоит флаг _USE_LINKED_LIST, то все выделяемые в объекте Vmem блоки организуются в связный список, что помогает при освобождении памяти. Но это освобождение происходит только при окончании работы машины, поэтому это можно считать не существенной оптимизацией. Гораздо интереснее работа Vmem без флага _USE_MSVCRT_MEM_ALLOC. В нем реализованы методы управления динамической памятью, описанные в первом томе Кнута гл. 2.5.

Я попытался провести тесты на быстродействие и объем памяти при различных флагах компиляции Vmem, однако не получил существенных различий в объеме потребляемой памяти и скорости работы - именно тогда я и понял, что это не самое узкое место в машине. Эта память используется лишь для создания статических структур, тоесть тех, которые одинаковы для любыых программ - это память под интерпретатор и виртуальную директорию. Начались поиски функций, создающих в памяти скаляр - от функции newSV я дошел до структур, описывающих сам скаляр. Он состоит из двух частей - SV_HEAD и SV_BODY. SV_HEAD есть почти всегда (например undef не имеет SV_HEAD), SV_BODY появляется тогда, когда скаляр поддерживает функциональность, для которой не достаточно SV_HEAD.

SV_HEAD имеет следующую структуру:

Названия полей
 SvHead
1 ссылка на SvBODY
2 счетчик ссылок
3 флаги (тип, …)
 SvHeadUnion
4 IV
 UV
 указатель на SV
 указатель на char
 указатель на массив или хеш

Заголовок состоит из четырех полей: ссылки на SvBody может и не быть, если скаляр уже в заголовке содержит нужную ему информацию - то есть структура SvBody не создается для ссылок и, начиная с версии 5.10, 32-битное целое. Счетчик ссылок и флаги - 32-битные поля: первое нужно для сборки мусора, второе хранит все характеристики скаляра, например тип SvBody, на который ссылается заголовок, а также некоторые флаги, понять смысл которых можно только разобрав устройство функций Perl, которые их используют. Скаляры в специальной области памяти, называемой PERL_ARENA, которая имеет фиксированный размер при компиляции - 4 КБ. Первый SvHEAD каждого PERL_ARENA в поле "ссылка на SvBODY" хранит ссылку на следующюю область PERL_ARENA или ноль, в поле "счетчик ссылок" - число элемемнтов в этой PERL_ARENA. Таким образом все области PERL_ARENA - это связный список, ссылка на первый содержится в PL_sv_arenaroot. Когда мы создаем новый скаляр, заголовок для него выделяется в этих областях, в случае нехватки памяти создается новая PERL_ARENA. При удалении скаляра ставится флаг удаления в поле флагов, все свободные SvHead через первое поле объединены в связный список, ссылка на первый свободный заголовок содержится в PL_sv_root. Таким образом память от заголовков практически не освобождается.

В комментариях приведен следующий эпиграф: "A time to plant, and a time to uproot what was planted..." - "Время садить и время выкорчевывать то, что было посажено". Это относится к процедуре удаления и выделения новой структуры SvHEAD: plantSV - "посадить скаляр", внести его в связный свободных скаляров, unrootSV - "выдрать скаляр", исключить из свободных и использовать.

До сих пор говоря о создании новой PERL_ARENA не было сказано ни слова, как именно выделяется память. Выделение памяти под структуры, создаваемые при исполнении байт-кода происходит с помощью функции Newx, которая выводит нас на огромный файл malloc.c. Про то, как он работает, я расскажу в следующий раз. Но при первом прочтении комментариев становится ясно, что он просто пользуется обычным malloc для эффективного управления кусками памяти, выделяемыми в процессе работы. Ясно что это узкое место во всей машине и этот участок кода должен работать как можно быстрее.

Каково назначение различных структур SvBODY мне стало понятно не сразу. Но на самом деле эти структуры и есть то, что делает скаляры Perl такими гибкими: когда мы производим вычисления, данные хранятся в скалярах, работая с фалом, мы сохраняем дескриптор в скаляре, работая с объектами мы работаем со скалярами, строки хранятся в скалярах... Можно было бы хранить все даные сразу в одной единственной структуре, но тогда бы мы имели много неиспользуемых полей. В Perl сделано так: в файле sv.h содержатся описания различных структур для SvBODY - у всех разное назначение, структура, размер. В sv.c с помощью функции Perl_sv_upgrade можно преобразовать SvBODY одного типа в другой. По каким правилам происходит это преобразование, какие SvBODY существуют и для чего нужен каждый из них я расскажу в следующий раз. Сейчас рассмотрим, где и как хранятся SvBODY.

Структура arena_set имеет такой же размер как PERL_ARENA - 4 КБ. Все arena_set образуют связный список, но в отличие от PERL_ARENA хранит не сами данные, а специальные структуры arena_desc. При хранении SvBODY используется тот же принцип, что и при хранении SvHEAD, но "тела" скаляров имеют разные типы и поэтому хранятся они в разных местах. Структура arena_desc содержит ссылку на массив (такого же размера как PERL_ARENA), который и является памятью для SvBODY некоторого типа. Массив PL_body_roots содержит для каждого типа SvBODY ссылку на связный список свободных ячеек. Еще при создании Arena для arena_desc для некоторого типа SvBODY все свободные ячейки в Arena организуются в связный список, оканчивающися нулем. PL_body_roots при этом присваивается ссылка на первый элемент. Когда создается SvBODY некоторого типа свободная память для него ищется из связного списка PL_body_roots[Type]. При удалении или правильнее - освобождении SvBODY память от него вновь добавляется в связный список PL_body_roots[Type]. Таким образом мы имеем ту же ситуацию, что и с заголовками скаляров, с той лишь разницей, что свободные блоки собираются отдельно для каждого типа SvBODY.

Отметим некоторые фичи, реализованные в операциях хранения SvBODY. “Ghost fields” - некоторые SvBODY содержат в начале структуры неиспользуемые поля, тогда их можно хранить в меньшего размера памяти, чем нужно для хранения самой структуры. Для них выделяются специальные арены, а при обращении к ним смещение полей пересчитывается, чтобы иметь доступ к нужным полям. Некоторые большие и редко используемые SvBODY вообще не хранятся в Arena, память для них выделяется отдельно. А некоторые скаляры вообще не имеют SvBODY.

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

Если в ходе изложения были допущены ошибки или неточности - можно писать на
awengar[at]gmail.com .