json_encodeとserializeの比較

データの受け渡しにJSONを使うかserializeを使うかは、大体相手や内容によって変わる。
外部アプリケーションに渡すならばJSONXMLを使うし、PHPのオブジェクトを保存するならば基本的にserializeが便利だ。
しかし内部的に使うだけのデータを配列に入れていて、これを保存しておきたい(APCのキャッシュなどにではなくディスクに)という場合はどちらでも良いので、速度が気になってくる。PHPではJSONの形式の方がserializeよりも高速というのは昔から言われている。
原因のひとつは、serializeがPHP専用の形式なのに非文字列型にバイナリを使わずテキストに直して保存しているからだ。
とはいえデータによっては速いこともあるかもしれなく、そのような比較データは見つからなかったのでやってみた。

追記

PHP7以降でOPcacheが使用できる環境で読み込みが主ならば、var_export()したものをrequire()するのが圧倒的に速い。

<?php
# キャッシュ側(遅い)
file_put_contents(CACHE_PATH, '<?php return ' . var_export($data, true) . ';');
// 適宜 opcache_invalidate() する

# 読み込み側
$data = require(CACHE_PATH);

元の記事

やってみたのだが、得られた「intのunserializeが速い」という結果はFast serialization of data in PHPで既出だった。無駄なことをしてしまった。
もったいないので一応続ける。

各数値はJSONで文字列配列をエンコードしたときの速度を1とした相対的な数値で、少ないほうが速い。
strはASCII文字列、ustrはUnicode文字列、binはバイナリ、intは整数値、floatは浮動小数点数値、s>は名前付き配列、[int]は整数値の二次元配列を表す。詳しくは末尾のソース参照。
JSONはバイナリを扱えない。必要があればbase64_encodeなど*1を前処理で使うが、そのようなケースではserializeとの速度比較は意味がないため計測していない。

JSON JSON PHP PHP
enc dec enc dec
str  1.0  2.6  4.6  3.2
ustr  1.1  2.6  4.4  3.2
bin - -  4.5  3.2
int  3.0  7.1  5.2  3.6
float 16.6 10.1 28.5 27.5
floatS 14.1  4.3 14.8  4.9
floatM 13.8  4.3 19.1 25.4
floatL 14.2  9.1 27.3 26.5
floatE 14.5  5.8 47.2 32.2
s>int  3.5  8.6  5.4  3.7
s>[int]  3.1  7.6  5.6  4.1
s>s>[int]  3.9 10.9  7.0  6.3

予想通りほぼすべてでJSONの勝利だが、整数値のデコードはunserializeが速かった。JSONだと最初は浮動小数の値と区別が難しいからだろうか*2。読み込みメインの整数値と決まっていたらserializeにするのもよさそうだ。

floatでPHPの遅さが目立つが、内部的にJSONはfloatをsnprintf()で文字列化するのに対して、serializeではphp_gcvt()で複雑な処理を行って正確な値を10進浮動小数で保存しているためだ。そのため2進数表現も綺麗なfloatSではJSONに遜色ない。

<?php
require_once('Benchmark/Profiler.php');
mb_internal_encoding('UTF-8');
run_test();

function run_test($count = 5000, $elm = 1000) {
    $profiler = new Benchmark_Profiler();
    $profiler->start();
    $runSection = function ($name, $data, $callback) use (&$profiler) {
        $profiler->enterSection($name);
        $result = $callback($data);
        $profiler->leaveSection($name);
        return $result;
    };
    $jsonEncode = function ($data) {
        return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    };
    $jsonDecode = function ($data) {
        return json_decode($data, true);
    };
    $serialize = function ($data) {
        return serialize($data);
    };
    $unserialize = function ($data) {
        return unserialize($data);
    };
    foreach ([
        '[str]' => 'make_strings',
        '[ustr]' => 'make_ustrings',
        '[bin]' => 'make_binaries',
        '[int]' => 'make_ints',
        '[float]' => 'make_floats',
        '[floatS]' => 'make_floatsS',
        '[floatM]' => 'make_floatsM',
        '[floatL]' => 'make_floatsL',
        '[floatE]' => 'make_floatsE',
        '[s=>int]' => 'make_s_ints',
        '[s=>[int]]' => 'make_sas_ints',
        '[s>s>[int]]' => 'make_sasa_ints',
    ] as $type => $generator) {
        $data = call_user_func($generator, $elm);
        for ($iter = 0; $iter < $count; $iter++) {
            if (strpos($generator, 'bin') === false) {
                $encData = $runSection('json.e ' . $type, $data, $jsonEncode);
                $result = $runSection('json.d ' . $type, $encData, $jsonDecode);
            }
            $encData = $runSection('php.e  ' . $type, $data, $serialize);
            $result = $runSection('php.d  ' . $type, $encData, $unserialize);
        }
    }
    $profiler->stop();
    $profiler->display();
}

function make_floats($elm, $list = []) {
    $max = mt_getrandmax();
    while (--$elm >= 0) {
        $list[] = mt_rand(0, $max) / mt_rand(1, $max);
    }
    return $list;
}

function make_floatsX($elm, $list = [], $x) {
    $max = mt_getrandmax();
    while (--$elm >= 0) {
        $list[] = $x;
    }
    return $list;
}

function make_floatsS($elm, $list = []) {
    return make_floatsX($elm, $list, 1.5);
}

function make_floatsM($elm, $list = []) {
    return make_floatsX($elm, $list, 1.1);
}

function make_floatsL($elm, $list = []) {
    return make_floatsX($elm, $list, 0.1234567890123456);
}

function make_floatsE($elm, $list = []) {
    return make_floatsX($elm, $list, 1e-20);
}

function make_ints($elm, $list = []) {
    $max = mt_getrandmax();
    while (--$elm >= 0) {
        $list[] = mt_rand(0, $max);
    }
    return $list;
}

function make_s_list($callback, $elm, $list = []) {
    $chars = preg_split('//', 'abcdefghijklmnopqrstuvwxyz');
    while (--$elm >= 0) {
        $list[rand_string($chars) . $elm] = $callback();
    }
    return $list;
}

function make_s_ints($elm, $list = []) {
    $max = mt_getrandmax();
    return make_s_list(function () use ($max) {
        return mt_rand(0, $max);
    }, $elm);
}

function make_sas_ints($elm, $list = []) {
    $elm = intval(sqrt($elm) + 1);
    return make_s_list(function () use ($elm) {
        return make_ints($elm);
    }, $elm);
}

function make_sasa_ints($elm, $list = []) {
    $elm = intval(pow($elm, 1 / 3) + 1);
    $elm2 = $elm * $elm;
    return make_s_list(function () use ($elm2) {
        return make_sas_ints($elm2);
    }, $elm);
}

function make_strings($elm, array $list = []) {
    return make_binaries($elm, $list, ['A']);
}

function make_ustrings($elm, array $list = []) {
    $char = mb_convert_encoding("\x30\x00", 'UTF-8', 'UTF-16BE');
    return make_binaries($elm, $list, [$char]);
}

function rand_string(array $chars) {
    $end = count($chars) - 1;
    $chars[mt_rand(0, $end)];
}

function make_binaries($elm, array $list = [], array $chars = []) {
    if (!count($chars)) {
        $chars = [];
        for ($i = 0; $i <= 255; $i++) {
            $chars[] = chr($i);
        }
    }
    while (--$elm >= 0) {
        $string = '';
        for ($length = mt_rand(1, 32); --$length >= 0;) {
            $string .= rand_string($chars);
        }
        $list[] = $string;
    }
    return $list;
}

*1:他の方法として強引にUTF-8に変換したりする(80-FFをU+0080-U+00FFに割り当てる)。Base64の33%増に対して0%〜500%増、平均81%増となるのでお薦めではない(\x80以降が2バイト文字で倍加する一方、\x00-\x1fは\uXXXXにエスケープするため6倍になる)。

*2:JSONの数値は浮動小数だが、json_decode時に条件を満たすと整数型になる。