Swiftの所有モデルは、特に~Copyable属性でマークされた非コピー可能な型、具体的には構造体と列挙型のための明示的なライフタイム管理を導入します。関数パラメータが借用でマークされている場合、コンパイラは引数を関数呼び出しの間、共有の不変参照として扱い、元のバインディングを有効に保ち、戻り時に値のライフタイムは変更されません。これにより、所有権を移転したりコピー操作をトリガーすることなく、複数の読み取り専用アクセスが可能になります。
逆に、消費修飾子は、関数が値の所有権を取得することを示しており、呼び出し側のスコープ内でそのライフタイムを終了させ、元のバインディングへの後続アクセスを防ぎます。コンパイラは、明確な初期化分析と移動専用チェックを通じてこれを強制し、使用後解放エラーが実行時ではなくコンパイル時にキャッチされるようにしています。このメカニズムは、ユニークな所有権を追跡する必要があるファイルハンドルやネットワークソケットなどのリソース管理にとって重要です。
これらの修飾子の違いにより、Swiftは移動専用リソースのメモリ安全性を保証し、ヒープ割り当てオブジェクトに通常関連付けられる参照カウントのオーバーヘッドを排除します。
struct AudioBuffer: ~Copyable { var data: UnsafeMutablePointer<Float> let frameCount: Int } func analyze(buffer: borrowing AudioBuffer) { // 有効:借用した値から読み取る let firstSample = buffer.data[0] } func process(buffer: consuming AudioBuffer) -> AudioBuffer { // 有効:所有権を消費し返す buffer.data[0] *= 2.0 return buffer } var buf = AudioBuffer(data: allocateBuffer(), frameCount: 512) analyze(buffer: buf) // bufは使えるまま let processed = process(buffer: buf) // bufは現在未初期化 // analyze(buffer: buf) // エラー:bufは消費された後に使用されました
私たちは、厳格なレイテンシ要件を満たすために、複数のエフェクトステージ(リバーブ、圧縮、EQ)を通じて大規模なマルチチャンネルPCMバッファを処理するリアルタイムオーディオエンジンを構築していました。この初期のアプローチは、UnsafeMutablePointerを生のオーディオデータに含む標準のコピー可能な構造体を使用していましたが、これはステージ間でのバッファ重複中にかなりのパフォーマンスペナルティを引き起こしました。また、コピーされた構造体が基となるAudioBufferプールより長生きするリスクがあり、実運用において安全性の危険を生じさせました。
最初に検討した代替案は、手動維持カウントが付いた最終クラスで生バッファをラップする参照カウントを持つクラスベースの設計を使用することでした。これにより物理コピーは排除されましたが、原子参照カウントのオーバーヘッドとオーディオグラフノード間の潜在的な保持サイクルを導入し、リアルタイムスレッドに必要な決定論的なテアダウンを複雑にし、CPU使用率を増加させました。
2番目のアプローチは、UnsafeMutablePointerとUnmanagedリファレンスを介してC関数間で直接渡される手動メモリ管理を含み、Swift安全性を完全にバイパスしました。これによりオーバーヘッドはゼロになりましたが、メモリ安全性が犠牲になり、中間処理中にバッファがプールに戻される際の使用後解放バグをキャッチするには広範なデバッグが必要で、開発速度が大幅に遅くなりました。
最終的に、私たちは明示的な所有権注釈を持つ非コピー可能な構造体を採用しました:バッファを新しい状態に変換するステージには消費修飾子を、読み取り専用の分析ステージ(スペクトル分析)には借用修飾子を使っています。この解決策は、ヒープ割り当てオーバーヘッドを排除しつつ、Swiftのコンパイル時安全保証を維持し、ストレステスト中に検出されたランタイムメモリ違反がゼロで安定した6msの処理遅延を達成しました。
非コピー可能な型に対して、借用はinoutとどのように異なりますか?
両方とも基礎となるストレージへのアクセスを許可しますが、inoutは排他的な可変アクセスを強制し、値が呼び出し元に有効な状態で返されることを要求します。これは、一時的な可変借用を効果的に作成し、呼び出し元が再開する前に終了する必要があります。一方、借用は共有の読み取り専用アクセスを許可し、値を「返す」必要や再初期化の必要がないため、移動専用型に対して不変操作に適しています。
関数本体内で消費パラメータを複数回使用できますか?
はい、しかし重要な制約があります:一度消費されると、値は別の消費コンテキストに移動されたり返されたりした後は再使用できません。候補者はしばしば消費が即座の破壊を意味すると思い込んでいますが、パラメータは、別の消費パラメータに移動されるか、値として返されるか、またはスコープを外れるまで関数スコープ内では有効です。移動操作後にアクセスを試みると、Swiftの移動専用チェックにより単一所有権を確保するため、コンパイル時エラーになります。
なぜ借用パラメータをインスタンスプロパティに保存しようとするとコンパイラエラーが発生するのですか?
借用パラメータは呼び出し元のスタックフレームに結びついており、そのライフタイムは同期関数呼び出しの期間によって厳密に制約されています。このような参照をインスタンスプロパティに保存すると、関数スコープを超えてそのライフタイムを延長し、呼び出し元が戻った後にダングリングポインタが作成されてメモリ安全性を侵害してしまいます。Swiftは、借用パラメータが関数呼び出しを逃れることを許可しないことでこれを防ぎますが、消費パラメータは所有権を移転し、ヒープ割り当てまたは拡張されたライフタイムのプロパティとして保存できます。