ARM64X — это формат «толстого» бинарного файла Windows для 64-битных ARM‑систем, применимый к исполняемым файлам и DLL. Для DLL выбор однозначен, поскольку работать будет только одна из версий: загружается та версия DLL, которая соответствует процессу‑хосту. Если хост‑процесс использует Windows ARM64 ABI, то используется ARM64‑версия DLL, а если процесс основан на x86‑64 или использует Windows ARM64EC ABI, то загружается соответствующая срез‑версия для этой архитектуры.
Для исполняемых файлов система располагает выбором. Она может запустить процесс как ARM64 или как ARM64EC. Как заставить систему выбрать предпочтительную архитектуру?
Это может понадобиться, если у вас есть программа, скомпилированная как ARM64X, потому что у вас реализована модель плагинов, и вы хотите поддерживать плагины, написанные либо под ARM64, либо под x86‑64. Вы компилируете ARM64‑версию для ARM64‑плагинов и ARM64EC‑версию для x86‑64‑плагинов. Во время выполнения может выясниться, что пользователь передал плагин для другой архитектуры, и вы захотите перезапустить себя в подходящей архитектуре.
Это можно сделать с помощью атрибута PROC_THREAD_ATTRIBUTE_MACHINE_TYPE.
Ниже описана программа, которая принимает DLL в командной строке. Она пытается загрузить её как нативную для текущей архитектуры, но если это не удаётся, и нативная архитектура — ARM64, то она перезапускает саму себя как x86‑64, чтобы попробовать снова.
Если нам удаётся загрузить DLL — отлично, выполняем её как обычно. Если загрузка не удаётся из-за того, что DLL имеет неверный формат, то мы повторим попытку как x86‑64, если текущий процесс запущен как ARM64. Для этого создаём список атрибутов с PROC_THREAD_ATTRIBUTE_MACHINE_TYPE, значением которого является архитектура, которую мы хотим попробовать, а именно AMD64 (Windows‑название для x86‑64), и перезапускаем себя с той же командной строкой.
Если DLL не удаётся загрузить даже как x86‑64, то x86‑64‑версия нашей программы просто сдаётся и не пробует снова как ARM64. (Не стоит заставлять x86‑64‑версию снова пытаться как ARM64, потому что это создало бы бесконечный цикл.)
Можно воспринимать ARM64EC как «предварительно JIT‑ированный x86‑64 на ARM64». Это похоже на то, чтобы взять x86‑64‑бинар и скомпилировать его в эквивалентный ARM64‑код (предположительно с лучшей производительностью по сравнению с тем, что бы сделал эмулятор на лету). Вместо того, чтобы поставлять x86‑64‑версию, которую эмулятор должен переводить в ARM64, просто поставляйте уже переведённую версию.
На практике, вероятно, вы добавите меры предосторожности, чтобы предотвратить случайные «взрывы форков». Во время подготовки этого материала автор по ошибке несколько раз случайно устроил у себя на машине fork‑бомбинг.
Рэймонд участвует в развитии Windows более 30 лет. В 2003 году он завёл веб‑сайт под названием The Old New Thing, который стал гораздо популярнее, чем он мог себе представить, и это до сих пор вызывает у него смешанные чувства. Сайт породил книгу с тем же названием (Addison Wesley, 2007). Время от времени он появляется в аккаунте Windows Dev Docs в Twitter, рассказывая истории, которые не всегда дают полезную информацию.
Установщик, по идее, должен уметь выбирать правильную архитектуру во время установки. ARM64X необходим только потому, что ARM64 и x86‑64 сосуществуют в одном пространстве имён (у нас нет отдельных SysWOWAMD64 или «Program Files (amd64)»), поэтому в одних и тех же каталогах могут жить код только для amd64, только для ARM64 и код «dual».
ARM64EC также позволяет поддерживать совместимость с x64‑плагинами. Это очень полезно для больших пакетов, таких как Office, например. Вместо того, чтобы убеждать разработчиков плагинов пересобирать свои проекты под ARM64, можно использовать x64‑плагины «как есть» с минимальными затратами на пересборку и возможными затратами на межпроцессное взаимодействие.
Ещё один пример: функциональность виртуальной камеры в OBS Studio реализована регистрацией DLL, которая загружается другими процессами, но Windows не предоставляет способа использовать разную DLL в зависимости от архитектуры (это, вероятно, было бы несовместимым изменением), поэтому DLL приходится делать ARM64X, чтобы её можно было загружать как в ARM64‑процессы, так и в x86‑64/ARM64EC‑процессы. У OBS также большая библиотека плагинов сообщества, которые исторически были x86‑64, поэтому со временем самому OBS, возможно, придётся выпустить сборку ARM64EC или ARM64X, чтобы сделать именно то, что описано в этой статье.
ARM64X выглядит интересно, но ещё более интересно было бы иметь поддержку «толстых» бинарников и для других сочетаний архитектур. Например, единый исполняемый файл с x86‑64 и ARM64, который может запускаться на Windows как x86‑64, так и ARM64. По крайней мере, автору не встречались свидетельства того, что ARM64X‑бинары могут выполняться на x86‑64 Windows. Неясно, чем занимается проект cosmopolitan, но, похоже, это не ARM64X.
Некоторые разработчики уже пробовали делать ARM64X PE с x86‑64 как основной «view», и это выглядит многообещающе; возможно, это можно реализовать на уровне компоновщика. Другие эксперименты с существующим компоновщиком не привели к созданию единого бинаря, способного нативно работать и на x86‑64, и на ARM64 Windows. LLVM LLD, по‑видимому, сейчас тоже поддерживает ARM64X, и будучи проектом с открытым исходным кодом, его легче модифицировать под такие эксперименты, но у некоторых разработчиков нет доступа к системе ARM64 для тестов.
В комментариях также указывали на несоответствие в одном предложении первой части текста, где видимо обрезана фраза про то, какая версия DLL загружается для x86‑64 или ARM64EC хостов.
Поскольку основная идея ARM64EC — поддерживать смешанный x64 и ARM в одном процессе, утверждение «загружается версия DLL, которая соответствует хост‑процессу» может вводить в заблуждение. ARM64X также поддерживает хранение среза x64: ARM64X‑DLL, загружаемая в x64 или ARM64EC‑процесс, загрузит либо ARM64EC‑срез, либо x64‑срез, отдавая предпочтение ARM64EC‑срезу, если он присутствует.
Документация намекает, что ARM64EC может давать худшую генерацию кода по сравнению с нативным ARM64. Правила ABI ARM64EC задают отображение регистров x64 в регистры ARM64 и указывают, что ARM64EC будет использовать только те регистры, которые входят в это соответствие (за исключением доступа к TEB). Также ожидаются переходы между EC и нативным кодом, особенно в режиме ядра. Возможно, есть различия и в работе компиляторных интринсиков.
Ключевой вопрос для разработчиков: зачем вообще выпускать ARM64‑исполняемый файл? Учитывая низкое распространение Windows на ARM до появления ARM64EC, практичным решением может показаться считать ARM64EC единственным ABI AArch64 для Windows и выпускать только «толстые» бинарники x64 + ARM64EC. Но такой подход оказывается слабым, если есть существенные преимущества таргетирования нативного ARM64: например, плагины для аудиоприложений часто интенсивно используют плавающую арифметику, и автопараллелизация/векторизация может давать худшие результаты при таргетинге ARM64EC по сравнению с чистым ARM64.
Поэтому, если некоторые ваши клиенты пишут ARM64‑плагины, а другие — x64‑плагины (которые на ARM64‑системах всё равно преобразуются в ARM64EC либо эмулятором, либо JIT, либо предкомпиляцией), вам нужно уметь создавать два типа хост‑процессов: один для ARM64‑плагинов и другой для x64‑плагинов. Если у вас только один тип хоста, то половина плагинов ваших клиентов не запустится. И раз уж существует два типа хост‑процессов, возникает вопрос: как указать системе, какой именно тип вы хотите?
ARM64EC нужен прежде всего для более эффективной поддержки x64 DLL в x64‑процессе на ARM64‑CPU. ARM64‑процессы не используют ARM64EC и в этом не нуждаются. Запуск x64‑кода на ARM64 несёт расходы по производительности; ARM64EC снижает накладные расходы эмуляции x64 на ARM64‑устройстве, улучшая производительность и энергопотребление.
Windows App SDK добавил частичную поддержку ARM64EC, и даже при частичном переводе частей приложения на ARM64EC наблюдались заметные приросты производительности.
(В конце обсуждения приводятся дополнительные не относящиеся к теме вопросы и оповещения об удалении комментариев.)
