先日の記事「PHPのジェネレータの実装を調べてみた」で僕は次のように書きました。GeneratorクラスはIteratorインターフェースを実装しており、対応するPHPメソッドを持っています。Cで実装した場合のみ指定できるイテレータ関数も実装しています。このように両方が指定されている場合、foreachループではCの関数が呼ばれ、イテレータメソッドを明示的に指定した場合はPHPメソッドの方が呼ばれます。ところで、イテレータに対応するPHPメソッドとC関数となぜ2つとも実装する必要があるのでしょうか。実は、PHPメソッドの方だけ実装すれば正常に動作します。C関数を実装する理由は速度面のメリットからだというのが僕の理解です。C関数は関数ポインタで単に呼び出せるのに対し、PHPメソッドの呼び出しは命令実行器の状態保存・復元の必要があるなど、呼び出しのコストがやや高いのです。hnwの日記PHPのイテレータをforeach文でループさせる場合、内部的に呼ばれる処理の候補としてC関数とPHPメソッドと2種類あること、C関数の方が速度の観点で有利であること、ジェネレータを作った場合はC関数が使われることを紹介しています。別の言い方をすると、PHPでIteratorインターフェースを実装した場合は必ずPHPメソッドが使われますので、それよりは同じ動作のジェネレータを作った方が速いということになりそうです。本稿ではこれを確認してみましょう。ベンチマークテスト1から指定された数までの整数を順に返すようなイテレータとジェネレータを作り、その実行時間を比較してみました。5.5.5での結果は次の通りです。結果の単位は秒です(以降の結果も同様)。同じ機能を持つイテレータとジェネレータとではジェネレータの方がおよそ3倍速いことがわかりました。上の結果を両対数グラフにしてみました。当然ですが、ループ回数にかかわらずほぼ一定の速度比であることがわかります。(対数の差は比を表すため)。PHPメソッド呼び出しとC関数呼び出しの差ジェネレータよりイテレータの方が不利な理由として、ジェネレータのイテレーション処理はC関数が呼ばれるのに対し、イテレータの場合はPHPメソッド呼び出しになるためだと紹介しました。これはどれくらい不利になるのでしょうか。これを確認するため、ジェネレータでPHPメソッドを利用するようにしてみましょう。これはPHP本体を以下のように2行コメントアウトすれば実現できます。zend_generator_get_constructor;この修正をしたPHPで同じベンチマークテストを行った結果が以下になります。method)が上のパッチを当てたPHPによるジェネレータ実装です。この結果を見ると、PHPメソッドを利用するジェネレータはPHPで実装したイテレータよりも遅くなっていることがわかります。これは、イテレータに対応するPHPメソッドの呼び出し(1ループあたりnext(),valid(),current()の3回)に加え、ジェネレータ関数の再開・停止の分だけ遅くなっているためだと考えられます。generator(C)とiteratorとgenerator(PHP)の実行時間の比はおおよそ1:3:4程度です。PHP関数・メソッド呼び出しとジェネレータ関数の再開&停止のコストを同一視すると、関数の呼び出し回数の比だと言えそうです。forループとも比較してみる今回のイテレータは単純なforループでも実現できる内容です。ArrayIteratorに巨大な配列を渡しても実現できます。これらとも速度を比べてみましょう。速いように思われたジェネレータも、forループやArrayIteratorと比べると一桁遅いことがわかりました。遅い原因は、先ほどの推測と同じくジェネレータ関数の再開・停止のコストだと考えられます。したイテレータではメソッド呼び出し3回分程度、それぞれ速度面でペナルティがあるというのは知っておいて損はないでしょう。今回のベンチマークテストではループ内で何もしていないときの実行時間を比較していますが、実戦ではループ内で意味のある処理を行うでしょうから、これらの差は相対的に縮まるはずです。個人的にはこのレベルの速度差はそこまで重要ではなく、大抵の場合は読みやすさ・保守性の高さの方が重要だと考えています。forループより遅いとしてもジェネレータで書きたい状況はあるはずです。ArrayIteratorは確かに高速ですが、大きい配列を扱うとnewするときのコストが大きくなりすぎて使い物にならないこともあるでしょう(上のベンチマークテストはインスタンスを作った後のループ時間を測定しており、配列の大きさの悪影響は現れていません)。5.5以降では活躍するはずです。次の日>ページビュー2060131。