Атрибут @inlinable указывает компилятору Swift сериализовать реализацию функции в файл интерфейса модуля, позволяя телу функции быть скопированным непосредственно в клиентские модули на этапе компиляции, что позволяет осуществить агрессивные оптимизации, такие как специализированные универсальные типы и свертка констант. Однако поскольку инлайн-код должен разрешать все символы внутри единицы компиляции клиента, любые internal типы, функции или свойства, на которые ссылается функция @inlinable, должны быть помечены как @usableFromInline, что позволяет компилятору обращаться к ним без опубликования в качестве публичного API.
// Внутри модуля устойчивого фреймворка @usableFromInline internal struct InternalBuffer { @usableFromInline var storage: [Int] } @inlinable public func fastSum(_ buffer: InternalBuffer) -> Int { // Может получить доступ к внутреннему хранилищу благодаря @usableFromInline return buffer.storage.reduce(0, +) }
Это сочетание позволяет авторам библиотек предлагать нулевые затраты на абстракции, где универсальный код становится моноформизированным в клиентском бинарном файле, хотя это жертвует некоторой гибкостью ABI, поскольку тело функции становится частью стабильного бинарного интерфейса.
Команда, разрабатывающая высокопроизводительный фреймворк машинного обучения, нуждалась в том, чтобы предоставить клиентским приложениям универсальную функцию умножения матриц matmul<T: Numeric>, но профилирование показало, что накладные расходы на вызовы функций между модулями и отсутствие специализации снижали производительность на сорок процентов по сравнению с ручным написанием циклов. Библиотека распространялась в виде бинарного пакета Swift, поэтому оптимизации на уровне исходного кода не были доступны клиентам.
Один из подходов заключался в том, чтобы сделать все вспомогательные типы и функцию реализации public, открывая каждую деталь управления внутренним буфером и расчетами шага. Хотя это позволило бы выполнить инлайнинг, это навязало бы команде необходимость поддерживать эти конкретные внутренние типы как стабильный API навсегда, предотвращая будущую рефакторинг и загромождая публичный интерфейс деталями реализации, которые потребители никогда не должны трогать напрямую.
Другой рассматриваемый вариант заключался в использовании @inline(__always), который агрессивно инлайнит код внутри того же модуля, но не экспортирует тело функции в другие модули; это сохранило бы чистоту API, но не позволило бы компилятору клиента специализированно обрабатывать универсальный тип T для конкретных числовых типов, таких как Float16 или Double, оставляя накладные расходы на выполнение на этапе выполнения и не достигая целевых значений производительности.
Инженеры в конечном итоге пометили точку входа как @inlinable и аннотировали внутренние структуры буфера и вспомогательные арифметические функции с помощью @usableFromInline. Эта стратегия открыла достаточно деталей реализации для компилятора, чтобы позволить полную моноформизацию и инлайнинг в точках вызова клиентами, сохраняя символы вне публичной документации. В результате клиентские приложения достигли производительности, идентичной вручную развернутому коду на C, хотя размер бинарника фреймворка немного увеличился из-за дублирования кода между модулями, и команда приняла, что исправление функции потребует от клиентов повторной компиляции.
В чем фундаментальное отличие между @inlinable и @inline(__always) в отношении границ между модулями?
@inlinable — это контракт интерфейса модуля, который записывает тело функции в файл .swiftinterface, позволяя компилятору выпустить реализацию непосредственно в зависимые модули во время их компиляции, что является необходимым для специализации универсальных типов между модулями. В отличие от этого, @inline(__always) является лишь подсказкой оптимизатору для локальной единицы компиляции; она указывает оптимизатору выровнять стек вызовов внутри модуля, но не делает тело доступным для внешних компиляторов, что означает, что клиентские модули по-прежнему вызывают функцию через устойчивую индирекцию и не могут устранить накладные расходы на универсальную диспетчеризацию.
Почему Swift требует @usableFromInline для внутренних символов, на которые ссылаются функции @inlinable, а не просто выводит видимость?
Когда функция инлайнится в клиентский модуль, компилятор должен генерировать конкретные машинные инструкции для этого кода в точке вызова, что требует полной типовой метаданных и адресов символов для каждого упоминаемого объекта; internal символы намеренно исключаются из интерфейса модуля для обеспечения инкапсуляции. @usableFromInline действует как специальный уровень видимости только для компилятора, который открывает определение символа в файле интерфейса, не делая его доступным для исходного кода клиента, удовлетворяя требованиям генерации кода, сохраняя при этом конфиденциальность на уровне исходного кода и предотвращая случайное утечку API.
Как принятие @inlinable влияет на стабильность ABI и характеристики размера бинарного файла библиотеки Swift?
Пометка функции как @inlinable встраивает ее реализацию в ABI библиотеки, что означает, что любое изменение в теле функции — например, исправление ошибки или улучшение алгоритма — будет считаться изменением, разрушающим бинарную совместимость, требуя от всех клиентских модулей повторной компиляции для обновления, в отличие от устойчивых функций, где реализацию можно менять независимо. Кроме того, поскольку компилятор дублирует тело функции в каждой точке вызова во всех клиентских бинарниках вместо ссылки на единственный адрес общей библиотеки, @inlinable значительно увеличивает общий размер бинарного файла финального приложения, что делает его неподходящим для крупных функций утилит, вызываемых редко.