В данной статье были проведены запуски JIT-бенчмарков под двоичными трансляторами из x86 в ARM Microsoft Prism Emulator, Apple Rosetta 2 и FEX Emulator. Также была проведена оценка влияния динамических модификаций кода на производительность двоичной трансляции.
О проблеме модификации кода в двоичной трансляции
Производительность JIT-кода является весьма интересным показателем в контексте динамической двоичной трансляции. В данном случае производительность транслятора зависит не только от качества сгенерированного кода, но и еще от эффективности обработки модификаций кода гостевым приложением. Модификации кода во время исполнения приводят к необходимости удаления откомпилированного кода и его последующей повторной перекомпиляции. Это накладывает дополнительные расходы на работу таких приложений под двоичными трансляторами, причем довольно часто данный фактор является наиболее узким местом при профилировании.
С архитектурной точки зрения все динамические двоичные трансляторы устроены примерно одинаково. Весь оттранслированный код они сохраняют в специальном кеше, что позволяет не перетранслировать один и тот же гостевой код повторно во время исполнения. При любой попытке модификации кода гостевым приложением, транслятору необходимо привести оттранслированный код в кеше в соответствие с измененным кодом гостевого приложения. Один из наиболее распространенных подходов к решению данной проблемы представлен на диаграме ниже и заключается в следующем. Регионы памяти, с которых транслятором был набран гостевой год, закрываются на запись. Таким образом в дальнейшем любая попытка модификации кода, набранного с данного региона, будет приводить к генерации SIGSEGV сигнала и доставки его транслятору. Затем транслятор на основании информации о пришедшем сигнале определяет попытку модификации кода, удаляет весь набранный код с региона памяти и снова делает данный регион памяти доступным на запись. На практике описанный подход зачастую является узким местом при тестировании производительности JIT-кода, особенно в случае многопоточных приложений.

Тестовое окружение
Для запуска Apple Rosetta 2 использовался Mac mini 2020. MS Prism Emulator тестировался на виртуальной машине в облаке MS Azure. Для получения доступа к Prism облачная виртуальная машина была единственной доступной опцией, поскольку, к сожалению, не удалось найти выделенный сервер на ARM под управлением Windows. Транслятор FEX также запускался на виртуальной машине в облаке Amazon AWS. Более подробные характеристики тестовых стендов приведены в таблице:
MS Azure VM | Apple Mac mini 2020 | Amazon AWS VM | |
CPU | Cobalt 100 | M1 | Graviton 2 |
Количество ядер | 4 | 8 | 4 |
Частота (ГГц) | 3.40 | 3.20 | 2.50 |
ОС | Windows 11 Enterprise 10.0.26200 | MacOS Sequoia 15.6.1 | Linux Ubuntu 24.04.3 LTS Linux Kernel: 6.14.0-1018-aws |
Оперативная память (Гб) | 8 | 8 | 16 |
Пару слов необходимо сказать о выборе метрики для сравнения производительности трансляторов. Рассматриваемые решения являются продуктами, разработанными для разных операционных систем, причем у Prism и Rosetta исходный код закрыт. По этой причине отсутствует возможность сравнить их производительность напрямую. В результате чего было решено считать эффективность двоичной трансляции как отношение показателя, полученного при запуске бенчмарка под транслятором к показателю, полученному при запуске того же самого бенчмарка в нативном режиме. Теперь перейдем непосредственно к тестированию производительности трансляторов.
Java Renaissance (чем меньше – тем лучше, миллисекунды):
Среда выполнения для Java: MS JDK 11.0.29

Бенчмарк работает в многопоточном режиме, задействуя все имеющиеся на машине ядра. Как видно из таблицы, Prism демонстрирует более высокую среднюю производительность в сравнении с Rosetta и FEX. Однако часть тестов из данного бенчмарка не удалось запустить под Prism ввиду отсутствия поддержки необходимых библиотек под Windows, поэтому данные тесты не были включены в таблицу. Также отсутствуют результаты тестов finagle-chipher и finagle-http для FEX по причине падения транслятора.
Ниже для каждого из тестов представлены графики с зависимостями количества модификаций кода от времени работы теста. Для большей наглядности все тесты данного бенчмарка были разбиты на два множества по длительности запуска. В первое множество попали тесты, которые работают доли секунды. Естественным образом количество модификаций кода на них довольно мало, поэтому их влиянием на производительность двоичной трансляции можно пренебречь. Во второй группе тестов можно обратить внимание на тест dotty, который в течение нескольких секунд выполняет несколько тысяч модификаций кода. Возможно, такое интенсивное модифицирование кода как раз и является причиной того, что все рассматриваемые трансляторы демонстрируют на нем низкую производительность порядка 20% от нативного исполнения. Из графиков видно что данный бенчмарк активно модифицирует код в течение всего времени исполнения тестов.

Java SciMark 2.0 (чем больше – тем лучше, “попугаи”):
Среда выполнения для Java: MS JDK 11.0.29

Данный бенчмарк работает в однопоточном режиме. На SciMark наблюдается прямо противоположная ситуация. Rosetta более чем в два раза обгоняет по производительности Prism. FEX также уверенно обходит Prism.
Ниже представлены графики с зависимостями количества модификаций кода от времени работы теста. Данный бенчмарк запускался в двух режимах: small и large. То есть, например, small представляет собой запуск всех тестов из таблицы, помеченных как small. Аналогичная ситуация и для large. Из таблицы видно, что и small и large запуски выполняют примерно одинаковое количество модификаций кода. Причем это количество довольно незначительное, что говорит о том, что модификации кода не влияют на производительность данного бенчмарка.

Java DaCapo (чем меньше – тем лучше, миллисекунды):
Среда выполнения для Java: MS JDK 11.0.29

Бенчмарк работает в многопоточном режиме. На DaCapo трансляторы Prism и Rosetta показывают практически одинаковую среднюю производительность. Тесты cassandra, h2o, pmd, zxing остутствуют в таблице, поскольку их не удалось успешно запустить под транслятором Prism. Также отсутствуют цифры для тестов tomcat, tradebeans и tradesoap для FEX из-за падения транслятора на данных тестах.
Ниже для каждого из тестов представлены графики с зависимостями количества модификаций кода от времени работы теста. Из графиков видно что данный бенчмарк активно модифицирует код в течение всего времени работы тестов. Можно заметить, что кривые для тестов fop и spring имеют наибольшую скорость роста, и при этом в таблице данные тесты демонстрируют наименьшую эффективность двоичной трансляции в районе 20-30%.

SpecJVM 2008 (чем больше – тем лучше, количество операций в секунду):
Среда выполнения для Java: MS JDK 11.0.29

Бенчмарк запускался в многопоточном режиме, задействуя все имеющиеся на машине ядра. Таким образом для Prism и FEX тесты выполнялись в 4 потока, а для Rosetta – в 8 потоков. Здесь снова наблюдается явное преимущество у Rosetta.
По интенсивности модификаций кода видно, что присутствует ряд тестов, которые основную часть своего времени работы не модифицируют код. К таким тестам относятся все тесты, представленные на верхнем графике. На нижнем графике тесты выполняют значительно большее количество модификаций кода, однако они довольно равномерно распределены по времени, из-за чего практически отсутствуют ситуации одновременной модификации кода разными потоками. Как следствие, трансляторы демонстрируют хорошую производительность на данных тестах.

Scala Compiler Unit Tests (чем меньше – тем лучше, секунды):
Среда выполнения для Java: MS JDK 17.0.17
Версия компилятора Scala: v2.12.21
Система сборки: sbt v1.11.7

Бенчмарк работает в многопоточном режиме, задействуя все имеющиеся на машине ядра. Результаты для FEX отсутствуют, поскольку ни один из тестов не был завершен успешно на нем. По этой же причине отсутствуют графики с модификациями кода для данного бенчмарка.
Web Tooling Benchmark (чем больше – тем лучше, количество запусков в секунду):
Среда выполнения для JavaScript: NodeJS v25.2.1

Бенчмарк работает в многопоточном режиме, при этом нагрузка на ядра распределена неравномерно: один процесс активно работает, в то время как все остальные процессы практически ничего не делают. Из таблицы видно, что средняя производительность Prism и FEX находится примерно на одном уровне.
График с модификациями кода представляет собой суммарное количество модификаций кода по всем тестам, посольку не было возможности замерить каждый из тестов в отдельности. По графику видно, что на каждый из тестов в среднем приходится порядка 1400 модификаций кода в течение 17 секунд.

JavaScript Octane 2.0 (чем больше – тем лучше, “попугаи”):
Среда выполнения для JavaScript: NodeJS v25.2.1
Версия движка V8: 14.1.146.11-node.14

Бенчмарк работает в многопоточном режиме, однако на деле, аналогично Web Tooling, только один из процессов что-то делает, в то время как остальные потребляют незначительное количество вычислительных ресурсов.
График с модификациями кода в данном случае не был разбит по тестам по той же самой причине, что и в случае с Web Tooling. Если грубо оценить, то каждый тест идет в среднем 2.6 секунды выполняя порядка 270 модификаций кода. Понятно, что на таком масштабе оценивать эффект модификаций кода на производительность двоичных трансляторов смысла нет.

Выводы
По результатам тестирования можно сделать вывод, что трансляторы Microsoft Prism Emulator и Apple Rosetta 2 демонстрируют довольно хорошую производительность в среднем. Однако по сумме факторов Rosetta все же обходит своего конкурента. FEX на их фоне обладает меньшей эффективностью, и при этом некоторые тесты на нем не были завершены успешно.
Касательно влияния модификаций кода на производительность двоичной трансляции видно, что в ряде случаев при выполнении интенсивных модификаций кода в течение небольшого промежутка времени, влияние данного фактора становится довольно значительным, понижая производительность примерно в 2 раза.
За ценные замечания, сделанные во время подготовки данной статьи, хочу поблагодарить @Armmaster.
