Опыт переноса пары приложений на Apple Silicon

Волею судеб и компании Apple пришлось тут перенести пару приложений на Apple Silicon (M1) и этот опыт хочется описать.

Я не думаю, что сделал какие-то фундаментальные открытия, но вдруг кому-то данный текст будет полезен…

Перенос на ARM/создание ARM64-приложения

По счастью, все используемые нами библиотеки умеют собираться под ARMv8, то есть там нет жесткой привязки к x86.

Многие из них умеют в оптимизацию под ARM-NEON и в этом месте возможна засада:  на Linux/Android принято получать информацию о возможностях ARM-CPU через getauxval() или через чтение в /proc/, ничего такого на macOS нет. Зато на macOS нет (пока!) разнообразия процессоров, весь (один) процессор который используется умеет NEON.

Соответственно, нужно поискать определение типа/возможностей ARM CPU и лишнее оторвать. Из используемых библиотек, это пришлось делать только у libdeflate, у остальных либо можно убрать определение возможностей CPU (libpng), либо оно уже убрано под #ifdef (libjpeg-turbo).

Библиотеки «без SIMD-наворотов» просто собираются.

Некоторое количество веселых минут доставила библиотека oneTBB (бывшая TBB): версия 2021.01 (актуальный релиз) собирается, но

  1. Несколько тестов не проходит
  2. Они еще и неустойчиво не проходят, ну скажем из десяти запусков теста  4 успешных, а 6 – нет.
  3. Не проходят ВАЖНЫЕ для нас тесты (parallel-reduce, например).

Чтение issues как-то не помогло, ну то есть открытых issues для Apple Silicon я не увидел.

По счастью, master-ветка отличается в лучшую сторону: часть тестов все еще не проходит, но все они относятся к resumable tasks, которых мы у себя не используем (а значит и хрен с ними пока).

Теоретически, с TBB можно вернуться обратно на QtConcurrent, но мы только вот перешли в обратном направлении.

В остальном же: С++ приложение (RawDigger) просто собирается и просто работает. Изменения кода – две строчки, чтобы в About показывал, что оно ARM64.

SSE-код

C FastRawViewer все конечно веселее, потому что там МНОГО кода на SSE/AVX intrinsincs.

По счастью, весь AVX-код – под #ifdef (потому что для 32-битной версии мы AVX не компилировали), а значит надо перенести только SSE-код (который у нас SSSE3).

Опять же по счастью, есть готовые решения, из которых было рассмотрено два:

  • SIMDe: переносимая реализация SIMD.
    Предполагается, что все _mm_... заменяем на simde_mm… (аналогично с типами переменных) и все работает.
    Соответственно, делаем глобальный поиск-и-замену и все работает.
    На деле – быстро закопались в ругань компилятора, что известные ему SSE-вызовы определены не так – ну и бросили.
  • Sse2neon: оказалось прямо РЕШЕНИЕМ.
    Включаешь sse2neon.h вместо *intrin.h и все работает.
    Ну если совсем точно, то нужно в sse2neon.h еще убрать реализацию _mm_malloc/_mm_free, поскольку они в macOS есть и так – и на этом все
    Более не пришлось делать НИЧЕГО, весь SSE-код заработал и тесты проходят.

Помимо SSE-кода пришлось дотачивать

  1. Конверсию из FP32 в FP16 и наоборот. Поскольку в  NEON есть готовые инструкции для этого, нужно было написать примерно 10 строчек кода.
  2. (примитивное, поскольку процессор пока один) определение возможностей CPU, просто две однострочных функции, возвращающих «AVX нету» и «FC16 есть».
  3. Прибить гвоздями GPU capabilities (что есть поддержка FP16)

На этом, опять же, все. Получилось примерно три десятка ‘#ifdef ARM’ на весь проект.

Результат работает несколько быстрее, чем бинарная трансляция, такой уж большой разницы нет потому что на Apple M1 мы явно GPU-bound.

UPDATE: ускоренная (с прочищенными тормозными местами) версия FastRawViewer в native-режиме работает примерно в полтора раза быстрее, чем при бинарной трансляции, есть за что бороться.

Создание Universal Binary

Если вы собрали приложение под ARM (а под x86 оно и так собирается), то создание Universal Binary (одного бинарника под две архитектуры) – это чисто техническая задача.

Ее можно решать двумя способами

  1. Делать две сборки и склеивать результаты при помощи lipo
  2. Склеить составные части заранее и собирать собственно приложение сразу под две архитектуры.

Мы пошли по второму пути. Идти по первому пути нужно в том случае, если x86 и arm64 версии собираются с разными версиями macOS SDK (например, если вы хотите совместимости с очень старыми версиями Mac OS X), но это не наш случай.

Общие слова

Сборка через clang -arch arm64 -arch x86_64 – это, на самом деле, две последовательные компиляции с одними и теми же параметрами компилятора, порождающие сдвоенный выход: бинарник (объектник, библиотека) один, а в нем две секции под архитектуру.

Соответственно, никаких -DHAVE_SSE или -DHAVE_NEON в командной строке компилятора быть не должно, все подобные архитектурно-зависимые куски должны разруливаться не в командной строке компилятора (линковщика, архиватора), а в компилируемых исходниках внутри (#ifdef __aarch64__ … #else), как именно это сделать – ну дело десятое.

Библиотеки

Как я уже писал ранее, современные системы сборки (cmake, qmake, configure) в общем случае не приспособлены для сборки универсальных библиотек под разные архитектуры (именно потому, что часть конфигурации платформы обычно засовывается в командную строку компиляторов).

Удобнее оказалось не чинить это в каждой отдельной библиотеке, а

  • Собрать отдельно под каждую платформу (я это делал на родной платформе, но это не обязательно, кросс-компиляцию сейчас все умеют)
  • Склеить в универсальные бинарники (через lipo)

В принципе, даже и клеить не обязательно, линкер умеет -L ../x86/ -lblabla_x86 -L…./arm/ -l blabla_arm, так вполне можно линковать статически (динамически, скорее всего, тоже, но не пробовал; естественно для динамической линковки dylib должны иметь разные имена). Линкер при этом предупреждает, что библиотеки не той системы, но это предупреждение можно смело игнорировать.

Для склейки больших проектов есть удобная утилита makeuniversal которая клеит прямо папки (то есть все бинарники, библиотеки, dylib которые найдет в подпапках), исходно изобретена для склейки (огромного) Qt, но успешно клеит и проекты поменьше.

Собственно приложение

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

Все шаги после сборки (формирование полного app bundle без внешних зависимостей, подпись, нотаризация) ничем не отличаются от сборки под одну платформу.

До сих пор нет уверенности, что Universal binary лучше, потому что оно получается, очевидно, примерно вдвое больше. Плюс один – пользователю не нужно думать какую версию скачивать (а что он скачает на несколько /десятков/ мегабайт больше и что у него будет занято больше места на диске – ну переживет).

Comments

Слушай, а у меня тангенциальный вопрос. Я вот тут хочу зарелизить опен-сорс утилиту командной строки, на C.

С помощью GitHub Actions я проверил, что она в лёт собирается и проходит тесты на MacOS (другого доступа к MacOS у меня нет, на MacOS-виртуалке гитхаба без прямого доступа всё просто заработало «как на линуксе»).

И я задумался — почему бы мне к релизам выкладывать не только исходники и бинарник для Windows, но и бинарник для MacOS.

А как это принято делать? Я так понимаю, запаковать собранный clang'ом бинарник (один, без зависимостей) вместе с README.md и LICENSE zip'ом — это не MacOS-путь, да? Надо как-то сложнее? Под винду я собираюсь делать именно так...

Это все зависит от того, кто твоя аудитория.
Запаковать zip-ом - путь не очень хороший, потому что Safari после скачивания зипа его тут же распаковывает прямо в Downloads :), но почему бы и нет (лучше тогда паковать папку).
Если твои пользователи способны потом сделать sudo mv your-utility /usr/local/bin - ну и прекрасно.

Следующий шаг - делать .pkg-инсталлятор. Это, насколько я помню, очень просто (я пробовал, но мы не делаем) и тогда оно может само себя в /usr/local/bin положить.
Можно ли делать такой инсталлятор не имея macos под рукой я не знаю, но опять же макос в виртуалке - невелика проблема сейчас.

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

Следующий шаг - платить $99 в год Эпплу и инсталлятор подписывать :)

Слушай, а где про pkg читать? Я нашёл pkgbuild на макоси, но явно говорю ему что-то не то - PKG отвергается как ставящий файлы на системный том, ну и там хотелось бы свой README может добавить если можно. man-то я прочёл...

Подпись пока без надобности

Слушай, я нихрена не помню.
Я на pkg смотрел в расстроенных чувствах: Каталина требовала нотаризации, у меня с нотаризацией .dmg ничего не получалось сначала, ну я с тоски думал что вот буду теперь делать pkg
Там были какие-то утилиты для собрать pkg, разобрать pkg, документацию какую-то я тоже нашел без труда (может быть на вебе статьи чьи-то, может на apple), но не помню нихрена.
Ничего не записывал, промежуточные результаты (скрипты и проч) не сохранил тоже - потому что получилось нотаризовать dmg

Кстати, классический маковский "drag to install" тебе же тоже вполне подойдет, если тебе нужно поставить ровно один файл (собственно утилиту). Просто таргет для drag - не /Appliications а /usr/local/bin и все.

С pkg, поставив таки MacOS в виртуалку (для этого скрипт есть уже, ничего патчить руками не надо!) я разобрался процентов на 25, осталось, да, понять какая раскладка считается приличной для такой утилиты — "/Aplications/App" или "/usr/local/bin + /usr/local/share/doc"... Нигде толком не пишут, КАК ТУТ ПРИНЯТО. Везде по-умолчанию считается, что XCode сам всё правильно сделает. И что все знают что такое Bundle. А про консольный утиль вообще никто нигде не упоминает, типа «всё для этих бородатых в свитерах ставится из brew».

Ну в /usr/local/{bin,lib,man,include} все и ставятся, да.
/Applications - для тех, которые кликом открываются, не для command-line

С (очередной) стороны, все что command line - это вообще для тех кто в свитерах с оленями

Да как ни посмотришь вокруг, все, у кого свитер с оленем подороже, чем бабушка связала, — на макбуке.

Ну да, или леново или макбуки. А какие еще варианты то.

Dell XPS? :)

На самом деле, Леново что-то в последнее время совсем не радует, кажется, они потеряли последние крупицы духа Thinkpad. Серия T всё ещё квадратная, но уже ощущается поддельными ёлочными игрушками.

Или это просто старость.

Меня макбуки радуют. Вот который на M1 в частности тоже - экран, маложруч, нетяжел, все через один разъем

Меня категорически не радует макось. Делал к ней много подходов (ради RPP, в частности) и каждый раз — дикое отторжение.

Впрочем, ноутбуки меня тоже не радуют, любые, просто фактом своего существования. Ну как можно работать и не сломать себе запястья и шею при таком расположении экрана и клавиатуры!? Как можно добровольно работать на одном экране?! Как можно работать с таким термопакетом!? Как можно работать без 10G сети!? (шучу, тут уже шучу). Как можно работать без хорошего внешнего DAC'а?! (а вот тут не шучу).

Если же использовать ноут как системный блок с подключённой внешней перефирией — то зачем? Можно взять или системный блок дешевле или мощнее.

Не, все ноуты — это пачка компромиссов, на которые мне непонятно зачем идти, если ты не разъездной админ или сейлз, которым объективно надо работать в разных местах.

И что самое грустное — ноуты — двигатели прогресса.

Ну не знаю. "Linuix, но все работает" же. В смысле - и переключение GPU и Wifi, и BT и color management и засыпает-просыпается нормально, сказка же. Трэкпад опять же лучший в мире.

Это всё так, если выбирать ноут. Но я не понимаю, зачем вообще выбирают ноут 75% (оценка снизу) тех, кто выбирают ноут. Не как редко используемую добавку к рабочей станции (если деньги есть почему бы нет), а как единственную рабочую станцию, как приоритет распределения бюджета на технику.

Я же вижу, что эти люди и в лучшее время использовали ноут за рабочим столом изо дня в день (часто не забирая домой с работы на ночь), а уж теперь, когда все дома сидят так и вообще :-)

У меня есть знакомые, которые всё время в дороге и гостинцах по всему миру — к ним вопросов нет. Но их мало.

Ну вот я тут выбирал хрень на M1, между Mac Mini и Macbook Air, разница в цене баксов наверное 400 или даже 500.

Выбрал ноутбук, потому что еще один компьютер только для сборки и тестирования на M1 - штука хорошая. Но в радиусе в полтора метра от меня этих компьютеров уже пять, что, еще шестой?

А с ноутбуком я могу выйти в палисадник покодить, тем более что M1 жрет очень мало.

Но в радиусе в полтора метра от меня этих компьютеров уже пять

Ага. Именно.

Ну а вот с ноутом я могу сбежать от этого великолепия!

При том что M1 - сука быстрый (на FRV 1.8: примерно как на i7-9700)

И надолго? Я вот иногда вынужден работать на ноуте — я больше 2-х часов не могу, очень некомфортное положение экрана относительно клавиатуры. И это не моя прихоть, это банальная физиология.

Можно говорить что второй экран — роскошь, но вот один большой экран подальше и клаивиатура поближе (и *вертикальное* расстояние между ними сантиметров в 15-20!) — это физиология.

Не знаю, ну в смысле мне же в нормальной ситуации не надо в палисадник надолго.

Но сегодня вот ковырялся за этим экраном полдня примерно, желание подключить монитор (а его надо просто подключить) было вялым, так и не включил.

Не знаю, ну в смысле мне же в нормальной ситуации не надо в палисадник надолго.

Ну вот я и говорю — такое я могу понять, если деньги есть, почему бы не иметь ЕЩЁ И нотубук.

Но куча осёдлых людей выбирают ТОЛЬКО ноутбук для постоянной работы. И вот это мне не понять никак.

Я не знаю :)
Тем не менее вот у меня с макосом - есть хакинтош, который надо включать и есть ноут. В принципе что-то быстро потестировать - мне быстрее на ноуте, открыл и тестируй....

Ну то есть, для меня это как курить примерно. Свои плюсы может и есть,но на столько нездорово и имеет столько прямых побочек (плохой нюх, и так далее), что я никак не могу понять, почему люди курят. Да, курят многие. За ноутбуками. Но для меня это какой-то мазохизм.

это всё здец как персонально. я например со своего 12" ноута почти и не вылезаю, при наличии намного более мощного компа с 2х20". и вообще я этот комп даже включаю не каждый день обычно.