Objective-Cの列挙の話。
Objective-C - NSArrayでfor(; ;)とかfor-inを使うのをやめて、enumerateObjectsUsingBlock:を使う - Qiita
とか
Objective-Cのいろいろな反復処理 - koogawa blog
とかで列挙の話を見たので。
最も高速な列挙の方法
最初に結論を書いておくと、Objective-Cにおいて最も高速な列挙の方法は、Objective-Cを投げ捨ててC言語で記述することです。
NSArray
やNSDictionary
は ランタイム関数の呼び出しがボトルネック になります。
文字列を列挙するだけならNSString
のNSArray
よりも、char*
の配列を扱ったほうが圧倒的に早いです。
NSEnumerator
Objective-C 1.0から存在するプロトコルで、基本的な列挙方法です。
objectEnumerator
で列挙子を取得して、nextObject
で要素を取得します。
Qiitaの記事には、
enumerateObjectsWithOptions:usingBlock:を使うと要素を後ろから見れます
とありますが、reverseObjectEnumerator
を使えばNSEnumerator
でも逆順の走査は可能です。
しかし先に述べたように、要素を取り出すたびにメソッドを呼び出す(=ランタイム関数を経由する)ことがボトルネックになります。
NSFastEnumeration (高速列挙)
そこでObjective-C 2.0ではNSFastEnumeration
プロトコルが追加されました。
Effective Objective-Cでは、
この項では、このメソッドがどのように動作するのかを完全に説明することはできない。しかし、インターネットのきちんとしたチュートリアルなら、このテーマを詳しく説明している。注意すべき重要ポイントは、このメソッドがクラスに対し同時に複数のオブジェクトを返すことを認めていることだ。このため、反復処理ループは、以前のものよりも効率がよくなる。
とばっさりと説明を端折られています。
要するにmalloc
されたポインタが引き渡されるので、そこに入るだけデータを突っ込んで引き渡します。このプロトコルに準拠するのは、C言語で実装するのと大差なく、非常に面倒です。
つまるところ、「メッセージパッシングがボトルネックだから、C言語で直接書けば高速だよね」を公式にサポートしたのが、NSFastEnumeration
です。
NSFastEnumeration
に準拠しているオブジェクトをfor in
文で列挙すれば、高速列挙になります。
enumerateObjectsWithOptions:usingBlock:
無名ブロックに処理を書くことで、列挙を並行処理化します。
その性質上、実行順序の保証はなく、特定のindex
の要素を削除するような、配列そのものに対する操作を書くことはできません。
個人的には、無名ブロックを作って列挙を行うという発想が、
1 to: 5 do: [:x | Transcript show: x].
Smalltalkを彷彿とさせる書き方なので好んで使ってます。
パフォーマンス的にNSFastEnumeration
とどちらが優秀なのか気になるところですが、並行処理が絡むので結果はデータ量(スケジューリングでボトルネックが発生する)や、ハードウェアの影響が大きいです。
そもそもパフォーマンスクリティカルな箇所ならObjective-Cで書くのが既に間違ってるので、気にしてもしょうがない気がしてます。
dispatch_apply / dispatch_apply_f
GCDを使って列挙処理を書くこともできます。
この関数については、並列プログラミングガイドにfor文をGCDで置き換えるメリットや注意点が日本語で書かれています。しかも無料です。素晴らしい。
まとめ
Objective-Cにおいて、NSFastEnumeration
に準拠したコレクションに対するfor
文とfor in
文とで行われる処理は完全に別物。前者は圧倒的に遅いので、index
が欲しいという理由でfor in
文をfor
文に書き換えるべきではない。
GCDを利用した列挙の並行処理は、NSFastEnumeration
に匹敵するパフォーマンスがある。ただし両者は別の性質を持つので、置き換えるものではない。