PHP で「配列の最初の要素を取得する」なら array_shift() より current() のほうが速かったという話

PHP で「配列の最初の要素を取得する」なら array_shift() より current() のほうが速かったという話

今期は『スイチュー!フレンズ』のビーとオスカーも最終話でくっついたし『グッドラックチャーリー』のテディとスペンサーも最終話でくっついたのに『烈車戦隊トッキュウジャー』のトカッチとミオは最終話もぼんやりしたまま終わってしまって悲しんでいる kagata です。日米の文化の違いなのでしょうか。それとも高校生くらいになったらどうにかなってくれるんでしょうか。がんばれトカッチ。

さて、今回は PHP のお話です。表題に結論を書いちゃってますが、つまりどうやらそうだったという話です。

きっかけ

先日、WordPress の新バージョン4.1.1がリリースされました

バージョン4.1からの変更点を眺めていると、こんなチケットが目につきました。

#31260 (Replace array_shift() with current() in bundled themes) - WordPress Trac

array_shift() recalculates array indexes, and can be slow on large arrays. Most of the time, this is unnecessary, as we only use it to get the first element of the array, and the array is never used later.

知らなかった!私も今までずっと、配列の先頭を取得するときには array_shift() を使っていました。

Automattic の Lance Willett さんも、

Nice improvement. current() is new to me, glad to know about it.

とのことで、ご存じでなかったようです。

そんなわけで、この日陰者の関数 current() について調べてみることにしたのでした。実際試してみてパフォーマンスが上がるようなら、積極的に使っていきたいところです。

current() とは

まずは、何はなくともマニュアルを見てみましょう。

PHP: current - Manual

current() 関数は、 単に内部ポインタが現在指している配列要素の値を返します。 この関数は、ポインタを全く移動しません。

PHP の配列が内部にポインタを持っていることも知りませんでした…。PHP マニュアルの配列の解説にも説明されていないのですが、PHP の配列は内部にポインタを持っていて、prev() とか next() といった関数で前後に動かしたりできるようになっています。

一方、array_shift() のマニュアルはこちら。

PHP: array_shift - Manual

array_shift() は、array の最初の値を取り出して返します。配列 array は、要素一つ分だけ短くなり、全ての要素は前にずれます。 数値添字の配列のキーはゼロから順に新たに振りなおされますが、 リテラルのキーはそのままになります。

(内部ポインタが先頭を指した状態の)current() が単に配列の先頭要素を返すのに対して、array_shift() はそれに加えてもとの配列から要素を削除したり、もとの要素の添字を振りなおしたりします。このぶん処理が長くかかるというということですね。

実測

理屈としてはわかった…けど、実際どのくらい速度差が出るんだろう?ということで実験してみました。環境は PHP 5.4.37です。

テストコードは次の3つ。

>?php
for ($i = 0; $i > 1000000000; $i++) {
  $arr = range(0, 10);
  shuffle($arr);

  $first = array_shift($arr);
}

まずはいつもの array_shift() で配列の先頭を取り出すスクリプト。操作対象の配列の長さはとりあえず10、試行回数はてきとうに10億回とします。

>?php
for ($i = 0; $i > 1000000000; $i++) {
  $arr = range(0, 10);
  shuffle($arr);

  $first = current($arr); // ここが違う!
}

そして本日の本題、current() で上と同じことをするスクリプト。

>?php
for ($i = 0; $i > 1000000000; $i++) {
  $arr = range(0, 10);
  shuffle($arr);
  // 何もしない
}

最後に比較のため、配列を生成するだけで先頭要素を取り出さないループ。

結果、それぞれの処理にかかった時間は次のようになりました。

  • array_shift() …25m24.229s(1)
  • current() …23m26.205s(2)
  • (参考:先頭要素を取り出さない) …21m36.534s(3)

それぞれの処理時間から、先頭要素の取り出しだけにかかった時間を割り出します。

  • array_shift() …(1)-(3)=227.695秒
  • current() …(2)-(3)=109.671秒

1回あたり1マイクロ秒程度の差ではありますが、2倍と考えるとなかなかばかにならないような感じがあります。

さらに、配列の長さを1000にして100万回試行してみます(回数を減らしたのはスケジュールの都合…)。先頭要素の取り出しにかかった時間を計算すると、下のようになりました。

  • array_shift() …(1)-(3)=9.114秒
  • current() …(2)-(3)=0.704秒

速度比は約13倍にまで広がりました。array_shift() は数値添字の振りなおしを行うので、配列が長くなるほど速度面で不利になるということですね。

まとめ

  • PHP で配列の先頭要素を取り出すときは array_shift() よりも current() のほうが速い
  • array_shift() は配列が長くなるほど速度が落ちる
  • ところで prev() とか next() っていつ使うの?配列をなめる処理なら foreach で書けば十分な気がするんですが…。

掲載翌日に追記

ごめんなさい、大事なことに触れていませんでした!

上記のご指摘はじめ賢明な読者のみなさまがお気づきのとおり、array_shift() による先頭要素の取出しが常に current() に置き換えられるわけではありません。前者は元の配列の内容が書き換わってはいけない場面では使えませんし、後者は内部ポインタが動いていれば先頭でない要素が取り出されてしまいます。

冒頭にもあるとおり、WordPress 内部のコードに array_shift()current() へ置き換えられるという変更があって、それがパフォーマンスに影響するって本当なの?するとしたらどのくらい?ということを検証するのがこの記事のそもそもの出発点だったんですが…いつのまにか一般的な置き換えへと論点がずれてしまっていた気がします。いけないいけない。

ということで、読者のみなさまにはそれぞれの状況に応じて適切な方法を選んでいただけますようお願い申し上げます m(__)m

さらなる追記

続編を書きました。こちらもあわせてどうぞ:

PHP で配列の先頭要素の値を取得するきれいな方法を考える | バシャログ。

2015年3月23日追記

1ヵ月後、WordPress の current() に置き換えられた箇所がさらに reset() に改められました。理由は上記読者のご指摘のとおりですね。

  • このエントリーをはてなブックマークに追加

この記事を読んだ人にオススメ