SwiftProgrammingiOS デベロッパー

Swiftがヒープリソースを含む値型を関数間で渡す際に冗長なメモリ複製を防ぐ仕組みは何ですか?

Hintsage AIアシスタントで面接を突破

質問への回答

Swiftは、ヒープに割り当てられたストレージをラップする値型に対して、Copy-on-WriteCOW)と呼ばれる最適化戦略を採用しています。代入時にすぐにディープコピーを実行する代わりに、言語は実際にインスタンスが変更されるまで複製を遅延させます。これは、値型が内部的に共有のバックグラウンドクラスインスタンスを参照し、isKnownUniquelyReferencedランタイム関数を使用して、参照カウントが1である時を検出することによって実現されています。変異が発生し、参照がユニークである場合、バッファはその場で変更されます。そうでない場合は、書き込む前にコピーが作成され、値の意味論を保持しつつ、早期コピーのパフォーマンスペナルティを回避します。

生活からの状況

我々のチームは、高性能な画像処理パイプラインを構築しており、大きなCVPixelBufferバックストアをラップするカスタムImage structを定義しました。プロファイリング中に問題が浮上しました:各フィルタの適用により、4K画像の中間コピーが3つ作成され、フレームごとに300MBのアロケーションを引き起こし、iPadデバイスでメモリ警告を発生させました。

このボトルネックを解決するために、3つの異なるアプローチを考慮しました。最初のアプローチは、Imagestructからclassに変換することでした。これにより、参照セマンティクスを使用してコピーが完全に排除されましたが、複数の処理チェーンが偶然に同じピクセルデータを共有し、一度に変更した際に深刻なスレッドセーフティバグを引き起こし、視覚的なアーティファクトやデバッグが難しい競合状態を引き起こしました。

2つ目のアプローチは、structの指定を維持し、UnsafeMutablePointermemcpyの最適化を使用して手動でディープコピーを実装しました。これにより厳格な値のセマンティクスを通じて安全性が確保されましたが、プロファイリングはターゲットの800%のCPU時間を消費することが示されました。各関数引数が12MBのメモリアロケーションとビット単位のコピー操作を引き起こしたためです。

3つ目のアプローチは、Copy-on-Writeセマンティクスを手動で実装しました。我々は実際のCVPixelBufferを保持するプライベートImageBuffer classを作成し、Image structがこのクラスへの参照を保持し、すべての変更メソッドが変更前にisKnownUniquelyReferencedをチェックするように実装しました:

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) } }

もし参照がユニークでなければ、まずバッファを複製しました。我々はこの解決策を選んだのは、Swiftの値のセマンティクスの安全性を保ちながら、読み取り専用の操作中に不必要なコピーを排除したからです。

その結果、メモリ圧迫が94%削減され、フレーム処理時間が1画像あたり120msから18msに短縮され、アプリが古いハードウェアで熱制限なしでリアルタイムのビデオストリームを処理できるようになりました。

候補者が見落としがちな点

なぜisKnownUniquelyReferencedを使わずに手動で参照カウントをチェックできないのですか?

多くの候補者は、手動で参照カウントを追跡したり、メモリアドレスを比較したりすることを提案します。しかし、isKnownUniquelyReferencedは単なるカウントチェックではありません。これは、メモリ操作の順序を最適化から防ぐコンパイラ挿入のバリアを含んでいます。この内蔵がないと、コンパイラがユニーク性チェックを最適化して省略したり、ランタイムがObjective-Cランタイムの相互作用や標準ARCカウントに見えない追加の未所有参照を維持するブリッジ変換のために誤った陽性を返すことがあります。

COWはSwiftの排他性強制とどのように相互作用しますか?

候補者は、COWがクラスを含むすべての値型に対して自動的に機能すると信じがちです。彼らは、Swiftの排他性ルールが変更に対して排他的なアクセスを必要とすることを見逃しています。カスタムCOWを実装する際には、変異が始まる前にisKnownUniquelyReferencedチェックを行い、チェックに対して原子的にバッファの置換を行う必要があります。このチェック中に複数の参照を保持することを違反すると、ランタイムの排他性違反が発生したり、ユニーク性検出において誤った陰性を引き起こすことがあります。

COWは並行コンテキストでコピーを防げない場合はいつですか?

Swift 5.5の並行性モデルでは、候補者はCOWがスレッドセーフな変異を提供すると仮定します。しかし、COWは単一のスレッド内でのみ安全性を保証します。アクターの境界を越えて値を渡したり、それらをSendableとしてマークする際に、コンパイラはアイソレーションを維持するために早期コピーを強制することがあります。さらに、バックグラウンドクラスにObjective-Cオブジェクトが含まれている場合、isKnownUniquelyReferencedObjective-Cの弱い参照の実装のため、保守的にfalseを返すことがあり、それにより不必要なコピーが発生し、所有権モデルを再構成して最適化する必要があります。