РИНЦ Форум ВКонтакте LinkedIn

Что в ядре проекта?

Насколько понятно самому, что это и зачем нужно, настолько же сложно передать другим. Компонент, который для проекта является ядром, в других системах спрятан в чём-то другом. Я называю его так:

Объектно-ориентированный динамический компоновщик и редактор связей

Это лучший способ назвать, который я придумал. А теперь объясняю, откуда такое название. Чтобы на что-то указать, нужно это что-то оттенить другим. Например, если бы все в мире говорили и писали только на русском, было бы очень сложно понять: что такое русский язык? Мы просто говорим. Мы просто пишем. И первая сущность, на которую я хочу указать, — это:

Процедурный динамический компоновщик и редактор связей

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

Давайте посмотрим, как он работает

На входе у нас есть файлы модулей. На входе у нас есть чистое виртуальное адресное пространство процесса. В файлах модулей описаны сегменты кода/данных. И процедурный динамический компоновщик где-то в виртуальном адресном пространстве размещает сегменты и производит релокацию. Затем процедурный динамический редактор связей соединяет модули. Пользуясь знанием о том, где компоновщик разместил другие модули, редактор связей прописывает указатели между модулями. Кроме ссылок на модули, связи идентифицируются именами процедур. Поэтому эти сущности названы процедурными.

Какие задачи разработчика решаются

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

Объектно-ориентированный динамический компоновщик и редактор связей

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

Также эту сущность можно встретить в виртуальных машинах: CLI, JVM. На этот раз виртуальные машины загораживают собой эту сущность. Постоянно что-то объемлющее загораживает, поэтому на сущность сложно указать. Проект исходит из предположения, что по соображениям производительности виртуальные машины отклоняются, а раз так, то надо вычленить нужный компонент и рассмотреть его отдельно.

Самое первое, на что нужно указать, — это предмет, с которым работает объектно-ориентированный собрат процедурного. У процедурного компоновщика это виртуальное адресное пространство. А у объектно-ориентированного? А это:

  • Образы объектов
  • Таблицы виртуальных методов

Образы объектов

Процедурный компоновщик компонует сегменты динамических модулей в виртуальном адресном пространстве процесса. Объектно-ориентированный компоновщик компонует наборы полей предков класса в адресном пространстве всех будущих объектов класса-потомка. Подобно тому, как сегменты модулей могут расти и сжиматься, сжиматься и расти могут наборы полей базовых классов, реализованных в других динамических модулях. И объектно-ориентированный редактор связей объясняет реализациям унаследованных методов, где найти известные им поля в объектах классов-потомков.

В System Object Model для этого существовала процедура somDataResolve. Получает на вход указатель на объект (this) и класс-предок, даёт на выходе указатель на набор полей, добавленных этим классом (self). Представляется возможным эту схему оптимизироваать.

Таблицы виртуальных методов

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

В System Object Model для этого существовали структуры ClassData и процедура somResolve. Каждый публичный метод, привносимый каким-то классом, получает индекс releaseorder, и по этому индексу в ClassData можно взять жетон метода. somResolve берёт на вход жетон метода и указатель на объект, на выходе даёт ссылку на реализацию метода. При этом в SOM как минимум версии 2.1 была сделана оптимизация: жетоны методов являются исполняемыми, с прототипом, идентичным прототипу метода. Так что не вызываем somResolve. Приводим жетон метода к нужному типу указателя и вызываем, передавая аргументы. А исполняемый жетон дальше разберётся, куда прыгнуть. В машинный кодах это похоже на вызов виртуальных методов COM. И по затратам обычно сопоставимо:

COMSOM
Запрос адреса интерфейсной VMT по адресу интерефейсаЗапрос адреса ClassData по адресу импортированной связи
Запрос адреса пред-метода по адресу VMTЗапрос адреса исполняемого жетона по адресу ClassData
Косвенный вызов пред-методаКосвенный вызов исполняемого жетона
Пред-метод декрементирует адрес указателя, чтоб переделать адрес интерфейса в адрес объекта
Пред-метод запрашивает адрес основной VMT по адресу объектаИсполняемый жетон запрашивает адрес VMT по адресу объекта
Пред-метод запрашивает адрес метода по адресу VMTИсполняемый жетон запрашивает адрес метода по адресу VMT
Пред-метод косвенно прыгает к реализации методаИсполняемый жетон косвенно прыгает к реализации метода

Но эти незначительные отличия в устройстве вызова радикально влияют на гибкость. Ведь в интерфейсной VMT COM некуда добавлять новые методы базовых классов, а ClassData у каждого класса свой, там есть, куда расти.

Оптимизации

Пользуясь знанием о том, есть ли у класса потомки, редактор связей может делать исполняемые жетоны, прыгающие сразу в реализацию. Пользуясь знанием о том, всегда ли класс наследуется первым, можно делать исполняемые жетоны, прыгающие по смещению в VMT. Но если это не так, то динамический поиск с кешем, как в Objective-C 2.0. В SOM очень стройно получается множественное наследование, и ещё лучше в PMtW. Удачнее подхода, когда множественное наследование только через интерфейсы.

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

Несовпадение импедансов

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

  • Если изменить базовый базовый класс и не пересобрать модули с наследниками, поведение программы некорректно.
  • Если изменить базовый базовый класс и пересобрать модули с наследниками, куча машинного времени убивается на перетрансляцию большого проекта. В Яндексе часы тратятся на перекомпиляцию, хотя, казалось бы, по сути, надо немного подвигать байтики.
  • В мире Delphi было популярно публиковать в закрытых кодах компоненты, но требовалось предоставить закрытый код отдельно для каждой версии Delphi. И когда версий Delphi в ходу стало слишком много, эта задача перестала быть подъёмной.
  • Если изменять базовый класс аккуратно, как в Qt, Delphi 2007, Objective-C 1.0 и Windows Runtime, используя предусмотрительно оставленные пустоты, разработчик вынужден соблюдать строгую дисциплину, а во время исполнения вместо цельного блока памяти образуется гирлянда связанных вспомогательных объектов, давящая на менеджер памяти.

А ведь всё может быть иначе. Процедурный компоновщик и редактор связей предоставлен операционной системой. Если мы поняли, что нам нужен объектно-ориентированный, то в текущих реалиях он будет оформлен как динамическая библиотека. После такого введения должно быть понятно, что это не обычная библиотека и не каркас, а нечто иное, более фундаментальное.

Кроссплатформенность

При эволюции библиотек классов растут или сжимаются наборы полей и методов. Похожим образом можно считать «версией 1.0» абстрактный набор классов для GUI, сети и файловой системы, а «версией 1.1» их специализацию под конкретную операционную систему. Таким образом, можно собрать один-два раза (32 и 64 бита) исполняемый или библиотечный модуль, и грузить его потом на разных операционных системах, и он будет вставать как влитой.

Двоичная трансляция на другие архитектуры процессоров

Как показано в моей статье, основной бич двоичных трансляторов — косвенные вызовы. Но также там есть и ссылка на проект, в котором эта стоимость испаряется, как только есть, куда положить подсказку двоичному транслятору, где оттранслированный код. Косвенные вызовы через ClassData не только гибки с точки зрения эволюции библиотек классов, но и их можно автоматически распознавать двоичным транслятором. А, распознав, избегать хеш-таблиц. И если все косвенные вызовы делать только через методы объектов, то x86 становится байткодом. Тяжёлая математика и графика делаются родными библиотеками, а логика приложения кодируется x86-байткодом.