Oberon Risc Machine - трёхадресная регистровая виртуальная машина, специально спроектированная для преобразования синтаксических конструкций Оберон-2М в машинные инструкции. Исходный текст программы преобразуется в промежуточный ORM-код, универсальное представление программы, независимое от целевой платформы. Для каждой целевой архитектуры процессора разработан индивидуальный бэкенд.
Процесс компиляции может быть как статическим, так и динамическим. Статическая компиляция и последующая сборка формирует готовый исполняемый файл для выбранного процессора и платформы (AOT-компиляция). Динамическая кодогенерация запускает модули непосредственно в среде разработки (JIT-компиляция).
В качестве прототипа Oberon Risc Machine выбран Risc компьютер нового тысячелетия Дональда Кнута MMIX. Из исходных 256 команд MMIX для ORM отобрано чуть меньше 100 наиболее подходящих инструкций, полностью покрывающих потребности трансляции Oberon-2M.
Архитектура ORM обладает рядом особенностей, делающих её идеально подходящей для выполнения поставленных задач. ORM поддерживает данные разрядностью 8, 16, 32 или 64 бита. Адресное пространство 32- или 64-битное в зависимости от реализации. Все операции производятся над регистрами, что характерно для risc - архитектур. Система команд строго трёхадресная, а длина каждой команды фиксирована и составляет 32 бита.
Регистровый файл ORM состоит из 64-битных универсальных регистров, способных хранить как целочисленные значения, так и числа с плавающей точкой. Регистр флагов исключен. Целые числа хранятся в дополнительном формате. Для вещественной арифметики используется стандарт IEEE 754. Всего доступно 256 регистров, что обеспечивает компилятору большую гибкость при распределении. Однако большое количество регистров требует адаптации при генерации кода реальных процессоров с меньшим числом физических регистров.
Стоит отметить, что многобайтовые числовые данные хранятся в порядке little-endian (младший байт по младшему адресу), в то время как строки хранятся последовательно, байт за байтом. Работа с памятью требует строгого выравнивания: 32-битные значения выравниваются по 4 байтной границе, 64-битные по 8 байтам, что критично для корректной работы на Arm64 и Risc-v.
Команды процессора занимают 32 бита и состоят из кода операции (opcode) и одного или нескольких операндов, в зависимости от вида операции. Операции кодируются однобайтовыми беззнаковыми целыми. Операнды также задаются беззнаковыми числами. ORM использует 5 форматов команд.
Формат 1 (основной): Помимо кода операции, задаются номера трех регистров, первый для результата выполнения операции, а второй и третий ссылаются на исходные данные (операнды). Операция может быть любой - арифметической, логической, записью или чтением из ячейки памяти или операцией межмодульного взаимодействия.
Формат 2 (с константой): Разновидность предыдущего формата. С тем отличием, что вместо последнего регистра указывается константа - знаковое целое значение от -128 до 127.
Формат 3 (регистр + константа): Кроме кода операции задается единственный регистр и двухбайтовая знаковая константа от -32768 до 32767. Этот формат используется в двух случаях. Для манипуляций с содержимым регистра с использованием двухбайтовой константы. Для реализации условных переходов.
Формат 4 (24-битная константа): Объединяет 3 последних байта в одну большую знаковую константу, состоящую из 24 бит. Используется для реализации "длинных" безусловных переходов.
Формат 5 (32-битная константа): Объединяет все 4 байта в одну большую знаковую константу, состоящую из 32 бит. Используется для занесения значения в адресный регистр, определяемый как вершина стека адресных регистров.
Соглашение о вызовах ABI в ORM отличается от распространенных стандартов. Хотя оно имеет сходства с stdcall (Win32), между ними есть существенные различия. Все параметры передаются через стек. При вызове процедуры сохраняются активные регистры. Полная ответственность за сохранение и восстановление регистров общего назначения, регистра стека и указателей фрейма лежит на вызывающей процедуре. Возврат значения из процедуры также происходит через стек. Спорное решение, но проведенные рандомизированные тесты не выявили преимуществ передачи параметров и возвращаемых значений через регистры.
Особое место занимает межмодульное взаимодействие. Для обращения к глобальным переменным или процедурам из другого модуля необходимо предварительно настроить значение базового регистра BP. В него заносится адрес загрузки вызываемого модуля в памяти (base pointer). Межмодульное взаимодействие - это фактически ключ к динамической кодогенерации и загрузке, как AOT-компиляции, так и JIT-компиляции.
ORM использует модуль в качестве минимальной единицы компиляции, что исключает возможность отложенной (ленивой) загрузки отдельных функций. Преимущества такого подхода: полная компиляция модуля при загрузке гарантирует, что все внутренние ссылки разрешены, а оптимизации применены корректно, надежность - нет риска частичной загрузки, производительность - оптимизации работают для всего модуля сразу, прозрачность - легко отлаживать и анализировать код.
Видимо, именно эта удачная особенность обеспечивает то, что время JIT-компиляции составляет лишь доли процента от времени чтения файла с диска в память. Замерить не получилось.
Компилятор полностью оригинальный, написан целиком с нуля, без использования заимствованного кода или сторонних включений. Компилятор генерирует нативный код для трёх современных архитектур: Amd64, Arm64 и Risc-v. Для каждой архитектуры разработан специализированный бэкенд, преобразующий ORM команды в соответствующие машинные инструкции.
Реализация поддержки Amd64, несмотря на его CISC-происхождение и ограниченное количество регистров, значительных трудностей не вызвала. Недорогой одноплатный компьютер Orange Pi 5B послужил целевой платформой для Arm64 архитектуры. Одноплатник StarFive VisionFive 2 для Risc-v архитектуры. Использовалось несколько различных версий Linux. А на Orange Pi 5B дополнительно установлена версия Windows 11, и тестирование проводится на ней также. Производительность нативного кода, генерируемого компилятором ORM, сопоставима с производительностью эквивалентного кода, генерируемого компиляторами Gcc и Clang.
Несколько обособленно от приведенной классификации стоит WebAssembly. С одной стороны, это платформа для портирования настольных приложений в web браузеры или JS среду исполнения, со своим форматом исполняемого файла и портируемыми JS-библиотеками. С другой стороны, это архитектура со своим ассемблером виртуальной Wasm машины.
Архитектура Wasm машины уникальна. Стековая архитектура виртуальной машины и низкоуровневый ассемблерный язык со строгой типизацией, наличием глобальных и локальных переменных, которым можно давать имена, высокоуровневые конструкции ветвлений if, then и циклов loop, четкое объявление процедур proc и отсутствие goto. В язык встроены операторы экспорта, импорта. Даже не все высокоуровневые языки предоставляют подобный набор средств.
Но как ни странно, все это синтаксическое богатство, а также отсутствие goto превращает трансляцию с регистровой архитектуры ORM в ассемблер стековой Wasm-машины в занятную головоломку. При этом результирующий Wasm-файл оказывается в полтора-два раза больше, чем нативный код для аппаратных RISC-архитектур. Плюс нет средств динамического управления памятью. Выделение и управление памятью должны полностью реализовываться внутри Wasm-модуля. Вот такой парадокс. Для импорта JS-библиотек реализован механизм бесшовной интеграции, аналогичный механизму динамических DLL-библиотек.
Повышенная производительность Wasm-приложений по сравнению с классическими JavaScript-приложениями, часто упоминаемая как ключевое преимущество, на деле является лишь приятным бонусом. Причем ее значимость обычно преувеличивают. Реальное повышение производительности возможно только для грамотно спроектированного, имеющего сложные указательные структуры приложения.
Главное преимущество заключается в том, что наличие Wasm-компилятора для языка высокого уровня позволяет создавать полноценные интерактивные web приложения, ничего не зная о JavaScript. Использовать весь накопленный багаж знаний и существующую кодовую базу языка программирования и возможности настоящего объектно-ориентированного программирования. Значительно ускорить выпуск сложных web приложений. Повысить надежность и переносимость веб-приложений.
ORM - это тщательно продуманное промежуточное представление, обеспечивающее эффективную трансляцию исходных текстов Oberon-2M в различные целевые архитектуры. Архитектура ORM сочетает простоту RISC подхода с особенностями, оптимальными для компиляции, что делает её эффективным и перспективным фундаментом для всего компилятора.
P.S. Хорошая новость для JavaScript разработчиков, которые не считают строгую типизацию достоинством, а wasm заслуживающим внимания. Более высокая производительность wasm приложений по сравнению с классическими JavaScript приложениями сильно преувеличена. Запустите и посмотрите сами.