"SwiftのArrayが実はすばらしかった"に対し、Copy-on-Write(以下CoW)について次のような指摘を受けました。
・CoWにはパフォーマンス上の問題がありC++では使われなくなってきている
・CoWはLazyにコピーが走るので実行時のパフォーマンスを予測しづらい
僕は、以下に述べる理由から SwiftがCoWを採用しても問題となりうるケースは限定的である と考えています。ただ、当然Swift経験は浅いですし、CoWについても詳しいわけではないので、まちがっていることがあれば指摘してもらえるとうれしいです。
##参照カウントとCoWの問題
参照カウント方式でメモリ管理をする場合、ただの変数にオブジェクト(への参照)を代入するだけで、新しいオブジェクトの参照カウントのインクリメントと古いオブジェクトの参照カウントのデクリメントが必要になります。また、参照カウントのインクリメント・デクリメントはアトミックに行われる必要があるため、その度にロックが必要になります。代入の度にこれが起こるので結構なオーバーヘッドです。
CoWは、コレクションに変更が加えられるときに参照カウントを確認し、コレクション(の実体)が共有されていればコピーを行います。参照カウントのチェックからコピーまではインクリメント・デクリメントや他の変更処理と排他的に行われる必要があるため、ここでもロックが必要になります。コレクションのコピーは比較的重い処理なので、この場合はロック範囲が大きくなります。
インクリメント・デクリメント自体はそれほど重い処理ではないので代入処理が定数倍遅くなるだけで済みますが、プログラムの至る所でロックがおこるのでマルチスレッド環境ではパフォーマンスの低下を招きやすくなります。
##C++とSwiftの事情の違い
C++はパフォーマンスを優先した言語です。必然的に、C++ではパフォーマンスが重要な処理が書かれることが多くなります。CPUを100%でぶん回して単位時間にどれだけの処理能力を持つかということが重要視されます。そのような言語において、参照カウントとCoWは重大な問題を引き起こしかねません。
SwiftはGUIのアプリを書く言語です。GUIの世界では平均速度がどれくらい速いかはそれほど重要ではありません。ボタンを押して0.0001秒で処理が終わろうと0.01秒かかろうとその差を体感することはできないからです。言い換えると、体感することの出来ない範囲のオーバーヘッドは許容できることになります。また、アプリは複雑な状態遷移を持つことが多く、いかにシンプルに状態をコントロールするかが重要になります。
##安全にアプリを書くために
コレクション(ArrayやDictionaryなど)がクラスだった場合、複数のオブジェクトが同じコレクションを共有したり、オブジェクトの内外から共通のコレクションを参照したりすると、カプセル化で隠蔽されているはずのコレクションがメソッドを介さずに変更されてしまう可能性があります。そのような設計は状態のコントロールを複雑にし、バグにつながりやすいため望ましくありません。
次の2点を守ってコードを書くことでコレクションの共有を避けることができます。
- コンストラクタ(またはsetter)で渡された(ミュータブルかもしれない)コレクションをコピーしてから保持する
- getterでコレクション(への参照)をそのまま返すのではなくコピーを返す
言い換えると、オブジェクトの入口と出口でコピーを徹底してコレクションが内外で共有されないようにするということです。
また、状態をコントロールする良い方法として、積極的にイミュータブルなクラスを導入することが挙げられます。変更が必要ないクラスをすべてイミュータブルにすることで状態をシンプルに扱えるようになります。イミュータブルなクラスでは1.と同じく、コンストラクタで渡されたコレクションをコピーして保持する必要があります。これらを守っているコードを「行儀の良い」コードと呼ぶことにします。
コレクションがクラスではなくstruct&CoWの言語では、単に代入したりreturnしたりすれば「行儀の良い」コードと同じことになります。
##「行儀の良い」コードの問題
コレクションがクラスだった場合、「行儀の良い」コードは安全性の代償としてコピーのコストを支払うことになります。
問題は、多くのケースでそのようなコピーは無駄になることです。コンストラクタに渡すコレクションはその直前で作られてすぐに破棄されることが多いでしょうし、オブジェクトからreturnされたコレクションも大抵は要素を取り出して表示したいだけで変更されることはありません。
自分で書いて自分で使うなら、「行儀の悪い」使い方をしない前提でコピーを省略してしまうこともできます。しかし、利用者が特定されない場合、安全のためにコピーを避けることができません。イミュータブルなクラスも、イミュータブルであることを保証するためにはコンストラクタでのコピーが必須です。
コレクションがstruct&CoWで実装されている場合、SwiftのArrayがすばらしいに書いたように「無駄なコピー」は発生しません。
##Lazyにコピーが走るとパフォーマンス予測が難しくないか
「行儀の良い」コードでは、あるコレクションに複数の参照が存在することはまれです。CoWのコピーは複数の参照がない限り実行されないので、「行儀の良い」コードを書いていればCoWでLazyなコピーが発生すること自体がレアケースだと思います。そのため、パフォーマンスが予測しづらい問題はほぼ起こらないかと思います。
##CoWのオーバーヘッド
「行儀の良い」コードでCoWのコピーがほぼ起こらないのであれば、CoWのオーバーヘッドは限定的です。コピーが必要ない場合、CoWは参照カウントをチェックしてすぐにロックを解除します。CoWによる参照カウントのチェックはインクリメント・デクリメントに比べれば頻度が低いので、CoWのオーバーヘッドは無視できるほどかと思います。
##参照カウントのオーバーヘッド
参照カウントのオーバーヘッドの影響は、GUIアプリをターゲットとしたSwiftではほぼ問題にならないように思います。参照カウントのオーバーヘッドがあるのはObjective-Cでも同じですが、数年間Obj-Cでアプリを書いてきて参照カウントに起因するパフォーマンス上の問題に出会ったことはありません。
そもそも、参照カウントのオーバーヘッドに限らず高級言語は様々なオーバーヘッドを持っています。本当にパフォーマンスが必要な処理はC/C++に切り出すはずです。参照カウントがないJavaであっても、パフォーマンス最優先の処理はJNIを使ってC/C++で書きますよね?
##そもそもコピーのオーバーヘッドも問題にならないのでは?
GUIでパフォーマンスが重要でないなら、「行儀の良い」コードのコピーコストも問題にならないのではないかと思うかもしれません。しかし、コピーはコレクションの要素数に比例する線形時間の処理であり、要素数が大きい場合に問題を引き起こします。
例えば、シャッターボタンを押したときにカメラから取得した800万画素の画像のバイト列(RGBAで3200万個のByte値を持つArray)をイミュータブルなImageクラスのコンストラクタに渡すという処理を書くと、コレクションのコピーによって一瞬UIが固まって感じられるはずです。Arrayがstruct&CoWの場合には、Imageクラスに渡されたバイト列はオブジェクトの外側では即座に破棄されるため、コピーが実行されることはありません。
オブジェクトの生成はプログラムの至る所で起こる処理です。その処理をC/C++に切り出すわけには行きません。アプリの状態の複雑さをコントロールするために「行儀の良い」コードを書くなら、CoWのコストよりもコピーのコストの方が顕在化しやすいのではないかと思います。
##結論
以上の理由から、SwiftではCoWがパフォーマンスに与える影響は限定的であり、コピーのコストの方が問題になりがちなのではないかと思います。アプリを安全に作るために「行儀の良い」コードを心がけるのであれば、コピーの代償を払わなくて良いCoWは良いパートナーのように思います。