Как ускорить Swift с помощью Conformances

31 января 2023

В рантайме Swift выполняет проверку соответствия протоколу, когда вы приводите тип к протоколу, например, с  as? или as!. Эта операция на удивление медленная, как подробно описано в моем предыдущем посте. В этой статье мы рассмотрим простой способ ускорить этот процесс примерно на 20%, не внося никаких изменений в ваш исходный код. А начнём с краткого обзора проверок соответствия протокола.

 

Обзор + улучшения iOS 16

Записи о каждом соответствии, которое вы пишете в исходном коде, сохраняются в разделе TEXT/const двоичного файла в форме, подобной этой:

struct ProtocolConformanceDescriptor {
  // Offset to the protocol definition
  let protocolDescriptor: Int32
  // Offset to the type that conforms to the protocol
  var nominalTypeDescriptor: Int32
  let protocolWitnessTable: Int32
  let conformanceFlags: UInt32
}

 

Типичное приложение может иметь десятки тысяч таких элементов. Многие из них соответствуют общим протоколам, таким как Equatable, Hashable, Decodable или Encodable. Когда в рантайме Swift встречает что-то вроде myVar as? MyProtocol (может отсутствовать непосредственно в вашем коде, общие функции, такие как String(describing:), внутри выполняют as?), он перебирает каждый дескриптор ProtocolConformanceDescriptor в двоичном файле, а также любые динамически связанные двоичные файлы. Сложность этой  операции O(n). В худшем случае, если вам необходимо найти запись о соответствии протокола для каждого типа, то сложность будетO(n2).

iOS 16 значительно улучшает этот процесс. Как я объяснял в предыдущем посте, iOS 16 предварительно вычисляет соответствие протокола при закрытии dyld, а в рантайме Swift консультируется с dyld перед запуском поиска O(n). Во время предыдущего сообщения в блоге компания Apple не выпустила исходный код dyld для iOS 16, но теперь мы можем увидеть действительную реализацию в функции _dyld_find_protocol_conformance_on_disk. Эта функция концептуально аналогична библиотеке zconform library, которая ускоряет эти проверки, используя хеш-таблицу. Эта хеш-таблица отображает типы в список протоколов, которым они соответствуют.

Хотя это улучшение есть в iOS 16, его трудно измерить на практике, потому что это поведение dyld отключено при запуске приложения из Xcode или Instruments. В Emerge есть local performance debugging tool (локальный инструмент отладки производительности), который решает эту проблему и может использоваться для профилирования приложений, у которых есть доступ к закрытию dyld.

Даже с учётом улучшений есть ещё 3 случая, когда вы можете столкнуться с медленным поиском:

  1. При первом запуске после установки/обновления приложения. Замыкание dyld ещё не построено, и все поиски соответствия всё ещё медлительны.
  2. Когда поиск соответствия приводит к nil. Можно было бы использовать _dyld_protocol_conformance_result_kind_definitive_failure, но быстрое сканирование исходного кода показывает, что эта возможность ещё не реализована.
  3. Если вы не используете iOS 16. Например, пользуетесь более старой ОС или используете Swift на платформе, отличной от Apple, включая Swift на стороне сервера.

С помощью простого файла порядка линковки исходников мы можем сократить время выполнения во всех трёх случаях.

 

Файл порядка линковки исходников

Файл порядка линковки исходников — это входные данные для компоновщика, которые ускоряют работу приложений за счет группировки кода, используемого вместе, в одной области двоичного файла. С файлом порядка линковки исходников ваше приложение получает доступ только к памяти, используемой кодом запуска приложения, а не считывает в память весь двоичный файл размером более 100 Мб. Этот принцип основан на понятии размера страницы памяти. Чтобы получить доступ к одному байту двоичного файла, загружается вся страница размером 16 КБ. Выгодно располагать необходимые данные на как можно меньшем количестве страниц. Ранее я подробно описал файл порядка линковки исходников.

Хранение секторов используемой памяти близко друг к другу также важно для повышения частоты попаданий в кэш. iPhone имеет несколько уровней кэша памяти, например, iPhone 7/A10 имеет следующую структуру [1]:


 
Специфика скоростей не публикуется компанией Apple и меняется из года в год, но некоторые тесты показывают, что повышение уровня может увеличить задержку в 5 раз [2].

 

Соответствие заказа

По умолчанию соответствие протокола распространяется по всему разделу __TEXT/__const двоичного файла. Это связано с тем, что каждый модуль в приложении генерирует свой собственный статический двоичный файл. Когда они связаны с окончательным приложением, двоичные файлы размещаются рядом. Данные из разных модулей не чередуются в исполняемом файле.

Давайте представим это на примере приложения Uber. Версия, которую мы используем, имеет 102 800 записей соответствия (в зависимости от размера раздела __TEXT/__swift5_proto) и раздел __TEXT/__const размером 12,7 МБ.

Соответствия на каждой двоичной странице раздела __TEXT/__const Uber.  По оси Y показано количество соответствий на каждой странице. Верхняя диаграмма представляет собой тепловую карту показателей соответствия.

На приведённом выше рисунке показано количество соответствий на каждой странице приложения Uber. Запись соответствия протоколу может различаться по размеру (зависит от таких деталей, как ассоциированные типы), но минимальный размер составляет 16 байт. Вы можете иметь максимум 1024 записи соответствия на одной странице памяти.

Интересно, что у Uber есть несколько пиков, когда страница не содержит ничего, кроме соответствия минимального размера. Это может быть связано с кодогенерацией, такой как внедрение зависимостей или сетевые модели, которые создают множество простых протоколов в одном модуле. Есть также пара диапазонов, не соответствующих требованиям, вероятно, из-за кода, отличного от Swift, в приложении. Ключевым выводом является то, что соответствия разбросаны по всему бинарному файлу, поэтому почти все страницы будут загружаться из памяти при перечислении соответствий.

Соответствия на каждой двоичной странице раздела Lyft __TEXT/__const

Точно так же на приведённом выше рисунке показано соответствие приложения Lyft. Хотя больших пиков нет, на каждой странице есть около 250 соответствий, за исключением одного диапазона, который, вероятно, не является кодом Swift.
Мы можем применить идею использования order files для группировки данных на как можно меньшее количество страниц к соответствиям и создать order files, который перемещает все соответствия на их собственные страницы.

Соответствия упорядочены в начале раздела __TEXT/__const

На приведённом выше рисунке показан результат использования файла порядка линковки исходников для группировки соответствий. Каждая из первых 250 страниц теперь содержит только дескрипторы соответствия протоколу, по 500 на страницу. Записи соответствия различаются по размеру, поэтому количество соответствий на странице не всегда одинаково. При таком порядке требуется загрузить менее половины раздела при выполнении поиска соответствия протоколу. На самом деле, общая память, используемая 250 страницами, меньше 4 МБ, поэтому в этом примере все они могут поместиться в кэш-память на уровне L3 в iPhone 7. В наших тестах совместное размещение таких соответствий привело к снижению производительности протокола более чем на 20%, время поиска соответствия на iPhone 7 под управлением iOS 15!

Вы можете создать файл порядка с таким результатом, проанализировав файл карты ссылок. Все соответствия протокола заканчиваются на Mc, поэтому вам просто нужны имена символов Swift, соответствующие этому шаблону, которые находятся в разделе __TEXT/__const. Вы можете написать подробный синтаксический анализатор структуры карты ссылок, но простая утилита grep также должна помочь:

cat Binary-arm64-LinkMap.txt | grep -v '<<dead>>|non-lazy-pointer-to-local' | grep -o '_$.*Mc$' > order_file.txt

 

Вот и всё! Теперь у вас есть файл порядка линковки исходников. Вы можете установить параметр сборки Xcode “Order File” на путь к этому файлу или ознакомиться с нашими документами с инструкциями по сторонним системам сборки. Это определённо стоит сделать, чтобы ускорить приложение для пользователей iOS 15 или первого запуска после обновления приложения на iOS 16. Если вы попробуете это улучшение в своем приложении, то я буду рад услышать об этом!

Свяжитесь с нами.

Emerge Launch Booster

iOS Launch Booster от Emerge автоматизирует процесс создания файла порядка линковки исходников и может сделать всё это за вас. Хотя оптимизация соответствия протокола в основном применяется к iOS 15 и первому запуску iOS 16, Launch Booster также включает в себя множество других оптимизаций, которые делают приложения быстрее для всех ваших пользователей.

[1] https://en.wikipedia.org/wiki/Apple_A10
[2] https://www.anandtech.com/show/14892/the-apple-iphone-11-pro-and-max-review/3

Оригинал статьи

Содержание