De @inlinable eigenschap instrueert de Swift compiler om de implementatie van een functie te serialiseren in het module-interfacebestand. Dit stelt het lichaam in staat om direct in clientmodules te worden gekopieerd tijdens de compilatietijd, zodat agressieve optimalisaties zoals generieke specialisatie en constante vouw mogelijk zijn. Echter, omdat inlined code alle symbolreferenties binnen de compilatie-eenheid van de client moet oplossen, moeten alle internal types, functies of eigenschappen die door de @inlinable functie worden benaderd, gemarkeerd zijn met @usableFromInline, wat ze blootstelt aan de compiler zonder ze als publieke API te publiceren.
// Binnen een veerkrachtig frameworkmodule @usableFromInline internal struct InternalBuffer { @usableFromInline var storage: [Int] } @inlinable public func fastSum(_ buffer: InternalBuffer) -> Int { // Kan interne opslag benaderen dankzij @usableFromInline return buffer.storage.reduce(0, +) }
Deze combinatie stelt bibliotheek auteurs in staat om goedkope abstracties aan te bieden waarbij generieke code in de clientbinaire wordt gemonomorfiseerd, hoewel het enige ABI-flexibiliteit opoffert omdat het functie lichaam deel uitmaakt van de stabiele binaire interface.
Een team dat een machine learning framework met een hoge doorvoer ontwikkelde, moest een generieke matrixvermenigvuldigingsfunctie matmul<T: Numeric> blootstellen aan clienttoepassingen, maar profiling onthulde dat overhead van functieaanroepen tussen modules en gebrek aan specialisatie de prestaties met veertig procent verminderden in vergelijking met handgeschreven lussen. De bibliotheek werd gedistribueerd als een binaire Swift-pakket, dus optimalisaties op broncode-niveau waren niet beschikbaar voor klanten.
Een benadering was om alle hulptype en de implementatiefunctie public te maken, waarbij elk detail van het interne bufferbeheer en stride-berekeningen werd blootgesteld. Hoewel dit inlining zou hebben toegestaan, zou het het team hebben gedwongen deze specifieke interne types als stabiele API voor altijd te onderhouden, wat toekomstige refactoring verhinderde en de publieke interface vulde met implementatiedetails die consumenten nooit rechtstreeks zouden moeten aanraken.
Een andere optie die werd overwogen, was het gebruik van @inline(__always), wat agressief code inlined binnen dezelfde module, maar het functie lichaam niet naar andere modules exporteert; dit zou de API schoon hebben gehouden, maar zou de clientcompiler niet hebben toegestaan om de generieke T te specialiseren voor specifieke numerieke types zoals Float16 of Double, waardoor runtime dispatch-overhead intact bleef en niet aan de prestatie-doelstellingen werd voldaan.
De ingenieurs markeerden uiteindelijk het toegangspunt met @inlinable en markeerden de interne bufferstructuren en rekenhelpers met @usableFromInline. Deze strategie bood net genoeg implementatiedetail aan de compiler om volledige monomorfisatie en inlining bij cliëntaanroepplaatsen mogelijk te maken, terwijl de symbolen uit de publieke documentatie werden gehouden. Het resultaat was dat clienttoepassingen prestaties bereikten die identiek waren aan handmatig uitgerolde C-code, hoewel de binaire grootte van het framework iets toenam door code duplicatie tussen modules, en het team accepteerde dat het patchen van de functie zou vereisen dat klanten opnieuw moesten compileren.
Wat is het fundamentele onderscheid tussen @inlinable en @inline(__always) met betrekking tot modulegrenzen?
@inlinable is een module-interfacecontract dat het functie lichaam in het .swiftinterface bestand schrijft, waardoor de compiler de implementatie direct in afhankelijke modules tijdens hun compilatie kan uitstoten, wat essentieel is voor generieke specialisatie tussen modules. In tegenstelling tot @inline(__always) is dit slechts een optimalisatiehint voor de lokale compilatie-eenheid; het instrueert de optimizer om de oproeppunt in de module te flatten, maar maakt het lichaam niet beschikbaar voor externe compilers, wat betekent dat clientmodules de functie nog steeds via veerkrachtige indirection aanroepen en de generieke dispatch overhead niet kunnen elimineren.
Waarom vereist Swift @usableFromInline voor interne symbolen die door @inlinable funkties worden aangesproken, in plaats van simpelweg zichtbaarheid af te leiden?
Wanneer een functie in een clientmodule wordt inlined, moet de compiler concrete machine-instructies genereren voor die code op het oproeppunt, wat complete type metadata en symboladressen vereist voor elke aangesproken entiteit; internal symbolen zijn opzettelijk uitgesloten van de module-interface om encapsulatie af te dwingen. @usableFromInline fungeert als een speciaal visibiliteitsniveau alleen voor de compiler dat de definitie van het symbool in het interfacebestand blootstelt zonder het toegankelijk te maken voor de clientbroncode, waardoor wordt voldaan aan de codegenereereisen terwijl de privacyniveau op broncode-niveau wordt behouden en onbedoelde API-lekkages worden voorkomen.
Hoe beïnvloedt het aannemen van @inlinable de ABI-stabiliteit en de binaire grootte van een Swift-bibliotheek?
Het markeren van een functie als @inlinable embed het implementatie in de ABI van de bibliotheek, wat betekent dat elke wijziging in het functie lichaam—zoals het verhelpen van een bug of het verbeteren van een algoritme—een brekende binaire wijziging inhoudt die vereist dat alle clientmodules opnieuw worden gecompileerd om de update te observeren, in tegenstelling tot veerkrachtige functies waarbij de implementatie onafhankelijk kan worden vervangen. Bovendien, omdat de compiler het functie lichaam op elke oproeppunt in alle clientbinaries dupliceert in plaats van te verwijzen naar een enkel gedeeld bibliotheekadres, @inlinable verhoogt aanzienlijk de totale binaire grootte van de uiteindelijke applicatie, waardoor het ongeschikt is voor grote, zelden aangeroepen hulpfuncties.