SwiftProgrammationDéveloppeur iOS

Comment Swift empêche-t-il la duplication redondante de mémoire lorsque des types de valeur contenant des ressources sur le tas sont passés entre les fonctions ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Swift utilise une stratégie d'optimisation appelée Copy-on-Write (COW) pour les types de valeur qui enveloppent un stockage alloué sur le tas. Au lieu de réaliser une copie profonde immédiatement lors de l'assignation, le langage retarde la duplication jusqu'à ce que l'instance soit réellement modifiée. Cela est réalisé en faisant en sorte que le type de valeur fasse référence à une instance de classe partagée en interne, tout en utilisant la fonction d'exécution isKnownUniquelyReferenced pour détecter lorsque le compteur de références est égal à un. Lorsque la mutation se produit et que la référence est unique, le tampon est modifié sur place ; sinon, une copie est créée avant l'écriture, préservant la sémantique de valeur sans le coût en performance d'une copie anticipée.

Situation de la vie réelle

Notre équipe construisait un pipeline de traitement d'image à haute performance où nous avions défini une struct Image encapsulant un grand magasin de CVPixelBuffer. Le problème est apparu lors du profilage : chaque application de filtre créait trois copies intermédiaires d'images 4K, entraînant des allocations de 300 Mo par image et déclenchant des avertissements de mémoire sur les appareils iPad.

Nous avons considéré trois approches distinctes pour résoudre ce goulet d'étranglement. La première approche consistait à convertir Image d'une struct en une classe. Cela a éliminé les copies en utilisant des sémantiques de référence, mais a introduit de graves bogues de sécurité des threads lorsque plusieurs chaînes de traitement partageaient et modifiaient accidentellement les mêmes données de pixels simultanément, entraînant des artefacts visuels et des conditions de concurrence difficiles à déboguer.

La deuxième approche conservait la désignation de struct mais mettait en œuvre une copie profonde manuelle en utilisant UnsafeMutablePointer et des optimisations memcpy. Cela garantissait la sécurité grâce à une stricte sémantique de valeur, mais le profilage montrait qu'elle consommait 800 % de temps CPU de plus que notre cible car chaque argument de fonction déclenchait une allocation de mémoire de 12 Mo et une opération de copie bit à bit.

La troisième approche a mis en œuvre manuellement les sémantiques Copy-on-Write. Nous avons créé une classe ImageBuffer privée pour contenir le véritable CVPixelBuffer, fait en sorte que la struct Image tienne une référence à cette classe, et mis en œuvre toutes les méthodes de mutation pour vérifier isKnownUniquelyReferenced avant la modification :

final class ImageBuffer { var pixels: CVPixelBuffer init(_ buffer: CVPixelBuffer) { self.pixels = buffer } } struct Image { private var buffer: ImageBuffer mutating func applyFilter(_ filter: Filter) { if !isKnownUniquelyReferenced(&buffer) { buffer = ImageBuffer(buffer.pixels.deepCopy()) } filter.process(buffer.pixels) } }

Si la référence n'était pas unique, nous avons d'abord dupliqué le tampon. Nous avons choisi cette solution car elle préservait la sécurité des sémantiques de valeur de Swift tout en éliminant les copies inutiles lors des opérations en lecture seule.

Le résultat a réduit la pression mémoire de 94 % et amélioré le temps de traitement des images de 120 ms à 18 ms par image, permettant à l'application de traiter des flux vidéo en temps réel sans limitation thermique sur du matériel plus ancien.

Ce que les candidats oublient souvent

Pourquoi ne pouvons-nous pas vérifier manuellement les comptes de références au lieu d'utiliser isKnownUniquelyReferenced ?

De nombreux candidats suggèrent de suivre les comptes de références manuellement ou de comparer les adresses mémoire. Cependant, isKnownUniquelyReferenced n'est pas simplement un contrôle de compte ; il inclut des barrières insérées par le compilateur empêchant les optimisations de réorganiser les opérations mémoire. Sans cet intrinsèque, le compilateur pourrait optimiser le contrôle d'unicité, ou le temps d'exécution pourrait retourner des faux positifs en raison des interactions du runtime Objective-C ou des conversions de pont qui maintiennent des références non possédées supplémentaires invisibles au comptage standard de ARC.

Comment COW interagit-il avec l'application d'exclusivité de Swift ?

Les candidats pensent souvent que COW fonctionne automatiquement pour tous les types de valeur contenant des classes. Ils oublient que les règles d'exclusivité de Swift exigent que les mutations aient un accès exclusif. Lors de l'implémentation de COW personnalisé, le contrôle isKnownUniquelyReferenced doit avoir lieu avant que la mutation ne commence, et le remplacement du tampon doit se faire de manière atomique par rapport au contrôle. En violent cela en maintenant plusieurs références pendant le contrôle, cela peut déclencher des violations d'exclusivité du runtime ou causer de faux négatifs dans la détection d'unicité.

Quand est-ce que COW échoue à empêcher la copie dans des contextes concurrents ?

Avec le modèle de concurrence de Swift 5.5, les candidats supposent que COW fournit une mutation sûre pour les threads. Cependant, COW garantit la sécurité uniquement au sein d'un seul thread. Lors du passage de valeurs à travers des frontières d'acteurs ou en les marquant Sendable, le compilateur peut forcer une copie anticipée pour maintenir l'isolement. De plus, si la classe de support contient des objets Objective-C, isKnownUniquelyReferenced peut retourner de manière conservatrice faux en raison de l'implémentation de références faibles de Objective-C, entraînant des copies inutiles qui exigent de restructurer le modèle de propriété pour optimiser.