SplFixedArrayの簡単な紹介とベンチマークとか

結論

SplFixedArrayは省メモリかつ基本的な固定長配列を実現する。

SplFixedArrayとは

PHP 5.0.0以降ならコンパイル時にわざわざ切らない限り、5.3.0以降なら必ず付属するライブラリSpl内の、固定長リストを実現するクラス。Cで書かれているので、PHPの機能で自前実装するより遥かに高速なのが特長。

環境をあまり選ばずに使えて、ちょっと面白い割にはそれほど知られていないようなので紹介してみる。

Iterator, ArrayAccess, Countableを実装しているので以下のように配列っぽく扱うことができる。ただしあくまで固定長なので、定義していないインデックスへの書き込みや参照はエラーになる。

$array = new SplFixedArray(6);
echo count($array); // 6

$array[0] = 'foo';
$array[6] = 'foo'; // throws RuntimeException

var_dump($array[0]); // string(3) "foo"
var_dump($array[5]); // NULL
var_dump($array[6]); // throws RuntimeException

foreach ($array as $index => $value) {
    ...
}

注意点

扱いとしては単なるクラスであって、配列互換ではない

例えばどうやってもempty()がtrueを返すようにはならないし、配列を期待する関数には渡せない。

$array = new SplFixedArray();
echo empty($array); // false
$array = new SplFixedArray(0);
echo empty($array); // false

$array = new SplFixedArray(10);
in_array('foo', $array); // raise error

メソッドは基本的なものしか提供されない

実装されているメソッドは、インターフェースに要求されているものにおまけが少々といった程度である。そのため少し複雑な処理を書こうとすると、自前で拡張してメソッドを書く必要がある。

ベンチマーク

実際配列と比較してどうなのか、という事でベンチマークを取ってみた。
各テストは100回行い、中間値をとっている。またテスト環境は以下の通り。

作成

array: 0.073484182357788

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $array = array();
}
echo microtime(true) - $start;

SplFixedArray: 0.2990140914917

インスタンス作成のコストがあるので結構重い。

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $array = new SplFixedArray();
}
echo microtime(true) - $start;

シーケンシャルリード

array: 0.11427307128906

$array = array();
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $array[$i];
}
echo microtime(true) - $start;

SplFixedArray: 0.082214117050171

$array = new SplFixedArray(1000000);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $array[$i];
}
echo microtime(true) - $start;

ランダムリード

array: 0.47042512893677

$array = array();
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $key = mt_rand(0, 999999);
    $array[$key];
}
echo microtime(true) - $start;

SplFixedArray: 0.43404412269592

$array = new SplFixedArray(1000000);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $key = mt_rand(0, 999999);
    $array[$key];
}
echo microtime(true) - $start;

シーケンシャルライト

array: 0.29079389572144

$array = array();
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}
echo microtime(true) - $start;

SplFixedArray: 0.25246906280518

$array = new SplFixedArray(1000000);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}
echo microtime(true) - $start;

ランダムライト

array: 0.56637191772461

$array = array();
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 999999);
    $array[$val] = $val;
}
echo microtime(true) - $start;

SplFixedArray: 0.43246507644653

$array = new SplFixedArray(1000000);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}

$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 999999);
    $array[$val] = $val;
}
echo microtime(true) - $start;

サイズを拡大しながらのシーケンシャルライト

array: 0.45435118675232

$start = microtime(true);
$array = array();
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}
echo microtime(true) - $start;

SplFixedArray: 0.46981692314148

固定のはずの配列長を毎回変更するという愚行の割にはほぼ配列並みの速さ。

$start = microtime(true);
$array = new SplFixedArray();
for ($i = 0; $i < 1000000; $i++) {
    $array->setSize($i + 1);
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}
echo microtime(true) - $start;

メモリ使用量

array: 144389304

$start = memory_get_usage();
$array = array();
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}
echo memory_get_usage() - $start;

SplFixedArray: 56000736

配列比38%に抑えられている。

$start = memory_get_usage();
$array = new SplFixedArray(1000000);
for ($i = 0; $i < 1000000; $i++) {
    $val = mt_rand(0, 1000000);
    $array[$i] = $val;
}
echo memory_get_usage() - $start;

総括

インスタンスの作成は配列よりかなり遅いので、小さいものを大量に作るには全く向いていない。
リードとライトは多くの場合で若干早いが、パフォーマンスの向上を期待できるほどではない。

メモリ使用量の少なさは注目に値するレベルで、特に各要素が小さく、要素数が異常に多い配列を置換するとかなり大きな効果が期待できる。
典型的にはDBやファイルからIDだけを100万件抜き出して配列に格納する、というような操作の時に使いどころがありそうだ。