HWW Portfolio

Software and Hardware Developer

Глобальная область видимости: почему в SOOT нет приватных имён

Оглавление


Введение

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


Что принято в языках общего назначения

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

Чтобы так работать, каждое определение нужно помечать: экспортируется оно или нет. Отсюда следует флаг в записи и две области видимости: область файла и общая, глобальная.


Что это значило бы для виртуальной машины

В SOOT вызов функции устроен просто. Инструкция call берёт функцию из регистра, а в регистр она попадает единственным способом: поиском по имени. Значит, каждое обращение к функции, методу или структуре превращается в поиск по имени, и этот поиск должен быть быстрым.

Теперь представим приватные имена. Экспортируемые определения можно индексировать в общей таблице. Но у приватных определений имена могут совпадать с приватными именами других модулей, поэтому в общую таблицу их класть нельзя. Остаётся три способа:

Любой из них даёт многоуровневый поиск: сначала область файла, потом глобальная. Усложняются и таблицы, и формат файла, и загрузка, а каждое обращение стоит дороже. Всё это ради инкапсуляции, которая в скриптах игры нужна мало: скрипты пишет небольшая команда, и защищать от неё внутренние функции другого скрипта обычно не требуется.


Решение: одна глобальная область

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

За это отвечает класс Globals. Он устроен так же просто, как решение: это плоская хэш-таблица из идентификатора строки в запись Symbol:

Globals не хранит значения и не владеет файлами: им владеет реестр модулей. Он лишь отвечает на вопрос «в каком загруженном модуле и по какому адресу лежит сущность с таким именем». Адрес действителен, пока модуль остаётся загруженным.

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


Анонимные сущности сегодня

Есть случай, который кажется исключением: структуры без имени. Если при инициализации поля-указателя создаётся новая структура, компилятор даёт ей сгенерированное имя. Искать такую структуру по имени не будет никто, но в нынешней схеме она тоже попадает в Globals.

Это даёт неожиданный бонус для отладки. Любую такую структуру можно найти в таблице, увидеть, где она лежит в памяти и какие у неё поля. Для инструментов это бесплатный способ заглянуть внутрь модуля.

Принцип «индексируется всё» в итоге получился последовательным: сгенерированное имя ничем не отличается от остальных.


Цена решения

У единой области видимости есть цена, и её лучше назвать прямо.

Компенсировать всё это можно культурой имён: префиксы по подсистемам, различимые имена для библиотечных и пользовательских функций. Для скриптов игры, которые пишет небольшая команда, это приемлемая плата. Для языка общего назначения такое решение было бы плохим, и я не предлагаю его как образец. Для узкой ниши, ради которой создавался SOOT, оно честно подходит.


Что дальше: локальные определения

Единая область не закрывает дверь для локальных сущностей. Идея в том, что сегодняшнее define заменяется явным выбором: определение может быть публичным или локальным (define-private). Для приватной сущности компилятор генерирует код, который ищет её не по имени, а загружает адрес определения напрямую. Имя разрешается при компиляции, а не во время выполнения, поэтому в рантайме такая сущность по сути анонимна и в Globals ей делать нечего. Двухуровневая область видимости при этом не возвращается: поиск по имени остаётся единственным способом найти публичное, а локальное никто по имени не ищет.

В ту же категорию попадают безымянные структуры-инициализаторы: на них ссылаются только по указателю.

Как это отразится на формате файла

Простейшее решение, флаг «не индексировать» в каждой записи, не подходит: все записи в файле выровнены по 64 бита, и один бит флага стоил бы лишних 8 байт на запись. Поэтому список определений в файле делится на две части: сначала идут публичные, потом приватные. В заголовке файла хранится одно число: сколько определений публичные. При загрузке модуль регистрирует в Globals только первую часть. Формат почти не усложняется, а память на флаги не тратится.

Имена приватных определений можно оставить в файле для инструментов: отладчик найдёт их, обойдя модуль, а общая таблица останется чистой.

Сроков я не обещаю: пока индексируется всё, и система отлично работает именно так.


Выводы

Изоляция модулей хороша для языков общего назначения, но каждая приватная сущность требует отдельного уровня поиска. Для виртуальной машины, в которой любой вызов начинается с поиска по имени, это дорого и сложно. Поэтому в SOOT одна глобальная область видимости, а возникающие проблемы решаются дисциплиной именования.

Решение не слишком чистое, зато простое и честное, и в этом оно похоже на остальную архитектуру SOOT. Если когда-нибудь понадобятся локальные определения, формат файла готов их принять почти без изменений: достаточно разделить список определений на публичные и приватные и записать их число в заголовок.


Статус проекта