Site icon AppTractor

Как ускорить Swift, упорядочив соответствия

Среда выполнения 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(n^2).

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

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

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

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

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

Упорядоченные файлы

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

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

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

Упорядочивание соответствий

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

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

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

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

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

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

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

Вы можете создать упорядоченный файл, проанализировав linkmap файл. Все соответствия протоколу заканчиваются на Mc, поэтому вам просто нужны символьные имена Swift, соответствующие этому шаблону, которые находятся в разделе __TEXT/__const. Можно написать подробный синтаксический анализатор структуры linkmap, но простой 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 будет значительной.

Источник

Exit mobile version