読者です 読者をやめる 読者になる 読者になる

なるようになるかも

力は多くの場合、その人の思いを超えない。

Objective-Cの列挙の話。

Objective-C - NSArrayでfor(; ;)とかfor-inを使うのをやめて、enumerateObjectsUsingBlock:を使う - Qiita

とか

Objective-Cのいろいろな反復処理 - koogawa blog

とかで列挙の話を見たので。

最も高速な列挙の方法

最初に結論を書いておくと、Objective-Cにおいて最も高速な列挙の方法は、Objective-Cを投げ捨ててC言語で記述することです。

NSArrayNSDictionaryランタイム関数の呼び出しがボトルネック になります。

文字列を列挙するだけならNSStringNSArrayよりも、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に匹敵するパフォーマンスがある。ただし両者は別の性質を持つので、置き換えるものではない。

参考文献