Ruby Internals: от исходного кода до исполнения

Ruby-программа проходит путь от текста до результата через несколько фаз: парсинг, компиляция в байткод, исполнение виртуальной машиной. По пути VM работает с объектами, классами, модулями, методами и блоками — каждый из которых имеет конкретное представление в C-коде CRuby. Понимание этих представлений объясняет поведение Ruby, которое на уровне языка кажется магическим.

Сама VM — это программа пользовательского уровня на C: для потоков она использует pthread, для аллокации — системный malloc, а для скорости горячего пути полагается на аппаратную иерархию памяти и ABI процессора. Поэтому дальше регулярно встретятся ссылки на Linux и архитектуру компьютера.

Порядок изучения

graph LR
    VM["VM (00-03)"] --> OM["Object Model (00-02)"]
    OM --> Methods --> Blocks --> Meta["Metaprogramming"]
    OM --> GC --> Collections
    OM --> Concurrency
    Meta --> JIT
    Collections --> JIT

VM: от текста до исполнения

Четыре фазы обработки Ruby-кода: текст → токены → AST → байткод → исполнение на стековой виртуальной машине.

Объектная модель

Как Ruby представляет объекты, классы и модули в памяти. Зависит от VM (VALUE, фреймы).

После Object Model — две независимые ветки. Можно читать в любом порядке.


Ветка A: Методы и метапрограммирование

Методы. Жизненный цикл метода: поиск, вызов, определение, удаление. Зависит от объектной модели (m_tbl, цепочка super) и VM (фреймы, ISeq).

Блоки и замыкания. Замыкания, Proc, lambda. Зависит от VM (EP, фреймы), компиляции (ISeq) и управления потоком (throw).

  • Блоки — yield, Proc.new, lambda, stack-to-heap promotion

Метапрограммирование. eval, instance_eval, define_method, refinements. Зависит от определения методов (CREF) и блоков (замыкания, EP).


Ветка B: Память и коллекции

Сборка мусора. Управление памятью: аллокация, mark-sweep, генерации, компактификация. Зависит от VM (VALUE, стек) и объектной модели (RBasic, flags).

  • GC — mark-sweep, generational, incremental, compaction, VWA

Коллекции. Внутреннее устройство встроенных типов Array, Hash, String. Зависит от объектной модели (VALUE, RBasic), GC (VWA, слоты, write barrier) и структур данных.

  • Array — RArray: embedded/heap-хранение, стратегия роста (×1.5), shared-массивы (CoW)
  • Hash — RHash: AR table (≤8 элементов), ST table (open addressing), переход между ними
  • String — RString: embedded/heap, кодировки, CoW, frozen strings, интернирование (fstring)

JIT-компиляция

JIT — компиляция горячего байткода в машинный код во время работы программы. В этой ветке она зависит от обеих линий: VM (ISeq, фреймы), методов (инлайн-кеш, CME), форм (shape_id), GC (code cache).

  • JIT-компиляция — YJIT (BBV), ZJIT (method-based), охранные проверки, инвалидация, кеш кода

Конкурентность

Как несколько потоков исполняют Ruby-код, какие гарантии даёт GVL, как Fiber и Ractor решают проблемы, которые не решают потоки. Зависит от VM (исполнения, барьеров памяти), объектной модели (shareable объекты для Ractor) и диспетчеризации методов (атомарность между байткод-инструкциями).

Как всё связано

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

VM vs Объектная модель: VM оперирует значениями типа VALUE на стеке. Объектная модель определяет, что стоит за каждым VALUE — RObject с массивом ivar и указателем klass. VM использует klass для диспетчеризации методов, shapes — для доступа к переменным.

Статическая структура vs Динамическое поведение: Объектная модель (классы, модули, shapes) задаёт структуру — где лежат методы и переменные. Методы и блоки определяют поведение — как код находится и исполняется. Метапрограммирование размывает эту границу: define_method создаёт метод из замыкания, instance_eval меняет self и лексическую область.

Кеширование на каждом уровне: Shapes кешируют доступ к ivar (shape_id → index). Method cache кеширует поиск методов (class serial → method entry). JIT-компилятор добавляет третий уровень — специализированный машинный код, который опирается на shape cache и method cache. Все три механизма оптимизируют горячий путь и инвалидируются при изменении структуры: переопределение метода сбрасывает method cache и JIT-код, изменение формы объекта сбрасывает shape cache и JIT-guard’ы.

Объектная модель vs Коллекции: Обобщённый RObject хранит ivar в массиве, а klass определяет поведение. Встроенные типы (Array, Hash, String) заменяют RObject специализированными структурами (RArray, RHash, RString), оптимизированными под конкретный паттерн доступа. Все начинаются с RBasic — поэтому klass, GC-флаги и shapes работают одинаково для любого объекта.

Коллекции vs GC: VWA из GC напрямую влияет на производительность коллекций: чем крупнее слот, тем больше данных хранится в embedded-режиме без malloc. Write barrier из generational GC срабатывает при каждой записи в массив или хеш. Compaction может переместить коллекцию в больший слот, вернув её из heap в embedded.

Sources

  • CRuby source repository: https://github.com/ruby/ruby
  • Pat Shaughnessy, 2013, Ruby Under a Microscope — Ruby internals from tokenization to GC