配列か連想配列かを判別する

PHPにおいて、ある配列が連想配列として使われているのかどうかを判別したい場合どうすればいいのか。

色々調べてたらこんな記事を発見しました。

<?php

function is_hash(&$array) {
    return array_keys($array) !== range(0, count($array) - 1);
}
テレパス・ラボ : 連想配列 判定関数(is_hash)

なるほど。

array_keysで配列のキー一覧を取得し、そのキー一覧が0からの連番になっているかどうかを比較すれば、対象の配列が配列なのか連想配列なのかを判別できるという理屈ですね。

確かにこれで判別ができそうです。

ですがひとつだけ気になる部分があって、array_keysやrangeを使って一時的な配列を確保しているのは無駄が多いのではないかと。

対象となる配列が大きければ大きいほどメモリ効率が悪くなりそうな気がします。

要件としては、対象の配列のキーが0からの連番になってるかどうかを調べたいだけなのでforeachでループしながら比較する方が効率良いのではないかと思い、実装してみました。

<?php

function is_hash(&$array) {
    $i = 0;
    foreach($array as $k => $dummy) {
        if ( $k !== $i++ ) return true;
    }
    return false;
}

こんな感じですね。

そして次にベンチマークをとってみます。

<?php
// PHP5.2.8

require_once "Benchmark/Iterate.php";

function is_hash1(&$array) {
    return array_keys($array) !== range(0, count($array) - 1);
}

function is_hash2(&$array) {
    $i = 0;
    foreach($array as $k => $dummy) {
        if ( $k !== $i++ ) return true;
    }
    return false;
}

$range = range(1,1000);  // 実験回数1000回
$array1 = range(0,1000); // 要素1001の配列

function is_hash_a () {
    global $range;
    global $array1;
    foreach($range as $i ) {
        is_hash1($array1);
    }
}

function is_hash_b () {
    global $range;
    global $array1;
    foreach($range as $i ) {
        is_hash2($array1);
    }
}

$bench   =& new Benchmark_Iterate;
$bench->run(1,'is_hash_a'); $result1 = $bench->get();
$bench->run(1,'is_hash_b'); $result2 = $bench->get();

print('is_hash_a -> '.$result1['mean'] . "\n");
print('is_hash_b -> '.$result2['mean'] . "\n");

要素が1001個ある配列を1000回繰り返し実行した場合のベンチマークです。

$ php bench.php
is_hash_a -> 0.265428066254
is_hash_b -> 0.106162071228

foreachで処理する方が2.6倍ほど早くなりました。

他にも色々な条件でやってみます。

// 要素が10個の配列を10万回実行
$ php bench.php
is_hash_a -> 0.319720983505
is_hash_b -> 0.169075965881

要素が少ない場合でもforeachの方が早いですね。

// 要素が30万個の配列を1回実行
$ php bench.php
is_hash_a -> 0.308174133301
is_hash_b -> 0.038535118103

要素が多い場合は速度の差が顕著にでます。

因みに配列の要素を100万個にしてみたら

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 524288 bytes)

というエラーがでました。どうやらarray_keysやrangeで確保するメモリの量が多すぎてプログラムが落ちるようです・・・。foreachなら100万個でも難なく処理できます。

で次に連想配列を渡したときのベンチマークですが、foreachの場合、キーが連番じゃなかったり数字じゃなかった時点で処理が終了するのでめちゃめちゃ早くなりました。

試しに文字列のキーを持つ10万要素の連想配列ベンチマークを取ってみます。

// 以下のような連想配列でベンチマークしてみる
// $array1 = array_merge(array('a'=>1),range(0,100000));
$ php bench.php
is_hash_a -> 0.933251142502
is_hash_b -> 1.59740447998E-5

比較にならないぐらい桁違いに早いですね。数万倍くらい?

結論としては

  • 配列の大きさに関わらずforeachの方が早い
  • 配列が大きければ大きいほどforeachの方が更に早い
  • 対象の配列が連想配列だった場合、foreachの方が桁違いに早い。

という感じなりますか。

foreach万歳ですね。ただひとつだけデメリットをあげるならば、ワンライナーじゃないというところになりますか^^;

さてこれでカンペキ!と言いたいところですが、実は実際のところ、連想配列として利用する配列のキー名に0という数字は使わないという前提条件がある場合、その対象配列の最初のキーが0かどうかだけをチェックすればそれだけでOKだったりするんですよね。

<?php

function is_hash_lite(&$array) {
    reset($array);
    list($k) = each($array);
    return $k !== 0;
}

こんな感じ。もちろん速度的にはピカ一で早いです。

厳密にチェックしたい場合はforeachを使い、前提条件がある場合は最初のキーだけをチェックする感じで良いんじゃないでしょうか。