Atrybut @inlinable instruuje kompilator Swift, aby zserializował implementację funkcji do pliku interfejsu modułu, co pozwala na skopiowanie ciała funkcji bezpośrednio do modułów klienckich w czasie kompilacji, umożliwiając agresywne optymalizacje, takie jak specjalizacja generyczna i składanie stałych. W każdym razie, ponieważ kod włączony musi rozwiązywać wszystkie odwołania symboli w jednostce kompilacji klienta, każdy typ, funkcja lub właściwość internal, do których uzyskuje dostęp funkcja @inlinable, musi być oznaczony jako @usableFromInline, co eksponuje je dla kompilatora bez publikowania ich jako publicznego interfejsu API.
// W obrębie odpornego modułu frameworka @usableFromInline internal struct InternalBuffer { @usableFromInline var storage: [Int] } @inlinable public func fastSum(_ buffer: InternalBuffer) -> Int { // Może uzyskać dostęp do wewnętrznego storage dzięki @usableFromInline return buffer.storage.reduce(0, +) }
Ta kombinacja pozwala autorom bibliotek oferować zerokosztowe abstrakcje, w których kod generyczny jest monomorfizowany w binarnym pliku klienta, chociaż poświęca pewną elastyczność ABI, ponieważ ciało funkcji staje się częścią stabilnego interfejsu binarnego.
Zespół rozwijający framework maszynowego uczenia o wysokiej przepustowości potrzebował udostępnić generyczną funkcję mnożenia macierzy matmul<T: Numeric> aplikacjom klienckim, ale profilowanie ujawniło, że przeciążenie wywołań funkcji między modułami oraz brak specjalizacji zmniejsza wydajność o czterdzieści procent w porównaniu do ręcznie napisanych pętli. Biblioteka była dystrybuowana jako binarny pakiet Swift, więc optymalizacje na poziomie źródła były niedostępne dla klientów.
Jednym z podejść było uczynienie wszystkich typów pomocniczych oraz funkcji implementacji publicznymi, ujawniając każdy szczegół zarządzania buforami wewnętrznymi i obliczeniami kroków. Choć to pozwoliłoby na włączanie, zablokowałoby zespół na wieczne utrzymywanie tych konkretnych typów wewnętrznych jako stabilne API, co zapobiegłoby przyszłemu refaktoryzowaniu i zagracało publiczny interfejs szczegółami implementacyjnymi, z którymi konsumenci nigdy nie powinni mieć bezpośredniego kontaktu.
Inną rozważaną opcją było użycie @inline(__always), które agresywnie włącza kod w obrębie tego samego modułu, ale nie eksportuje ciała funkcji do innych modułów; zachowałoby to czystość API, ale nie pozwoliłoby kompilatorowi klienta na specjalizację generycznego T dla konkretnych typów numerycznych, takich jak Float16 lub Double, pozostawiając przeciążenie rozruchowe nietknięte i nie spełniając celów wydajności.
Inżynierowie ostatecznie oznaczyli punkt wejścia jako @inlinable i oznaczyli wewnętrzne struktury buforów i pomocniki arytmetyczne jako @usableFromInline. Ta strategia eksponowała wystarczająco dużo szczegółów implementacyjnych dla kompilatora, aby umożliwić pełną monomorfizację i włączanie w miejscach wywołań klienta, jednocześnie utrzymując symbole z daleka od publicznej dokumentacji. Wynikiem było to, że aplikacje klienckie osiągnęły wydajność równą ręcznie rozwiniętemu kodowi C, chociaż rozmiar binarny frameworka nieco wzrósł z powodu duplikacji kodu między modułami, a zespół zaakceptował, że poprawienie funkcji wymagałoby ponownej kompilacji klientów.
Jaka jest zasadnicza różnica między @inlinable a @inline(__always) w odniesieniu do granic między modułami?
@inlinable to umowa interfejsu modułu, która zapisuje ciało funkcji do pliku .swiftinterface, pozwalając kompilatorowi na bezpośrednie emitowanie implementacji do zależnych modułów podczas ich kompilacji, co jest niezbędne dla specjalizacji generycznej między modułami. W przeciwieństwie do tego, @inline(__always) jest jedynie wskazówką optymalizacyjną dla lokalnej jednostki kompilacyjnej; instruuje optymalizator, aby spłaszczał stos wywołań w obrębie modułu, ale nie udostępnia ciała inny modułom zewnętrznym, co oznacza, że moduły klientów wciąż wywołują funkcję poprzez odporną indykcję i nie mogą wyeliminować przeciążeń związanych z wywołaniami generycznymi.
Dlaczego Swift wymaga @usableFromInline dla wewnętrznych symboli, na które odwołują się funkcje @inlinable, zamiast po prostu wnioskować o widoczności?
Gdy funkcja jest włączana w module klienckim, kompilator musi wygenerować konkretne instrukcje maszynowe dla tego kodu w miejscu wywołania, co wymaga pełnych metadanych typów i adresów symboli dla każdego odniesionego bytu; symbole internal są celowo wykluczone z interfejsu modułu w celu wymuszenia enkapsulacji. @usableFromInline działa jako specjalny poziom widoczności tylko dla kompilatora, który eksponuje definicję symbolu w pliku interfejsu, nie udostępniając jej kodowi źródłowemu klienta, spełniając wymagania dotyczące generacji kodu, a jednocześnie utrzymując prywatność na poziomie źródła i zapobiegając przypadkowemu wycieku API.
Jak przyjęcie @inlinable wpływa na stabilność ABI i charakterystyki rozmiaru binarnego biblioteki Swift?
Oznaczenie funkcji jako @inlinable osadza jej implementację w ABI biblioteki, co oznacza, że każda zmiana ciała funkcji—taka jak naprawienie błędu lub poprawa algorytmu—stanowi zmianę łamiącą binarnie, która wymaga, aby wszystkie moduły klienckie zostały ponownie skompilowane, aby dostrzegać aktualizację, w przeciwieństwie do funkcji odpornych, gdzie implementacja może być wymieniana niezależnie. Ponadto, ponieważ kompilator duplikuje ciało funkcji w każdym miejscu wywołania we wszystkich binarnych plikach klientów, zamiast odnosić się do jednego wspólnego adresu biblioteki, @inlinable znacznie zwiększa całkowity rozmiar binarny finalnej aplikacji, co czyni go nieodpowiednim dla dużych, rzadko wywoływanych funkcji użytkowych.