PHPで簡易ベンチマーク計測用ライブラリを書いた

投稿日:

5歳になる息子が毎日ち○こと言わないと気が済まないようで困っています。
そうなると下の子(妹)も真似するわけで、家では毎日ち○こ祭りです。

というわけで、掲題にある通りPHPで簡易ベンチマーク計測用ライブラリを書きました。
ソースはこちらです。

https://github.com/shimabox/SMBBenchmark

なんで今更感半端ないものを書いたかというと、ベンチマークを取りたいときに、いつもググっては、microtime()とかtime()を書くのが面倒臭いなと思ったからであります。
※PEARのベンチマークライブラリがメジャーっぽいけど、PEARが使えるのかとか気にするのもあれだし

ぶっちゃけネットで得られる情報の寄せ集めなので大したことは無いんですが、せっかく書いたので公開します。
README.mdを見てもらえれば使い方は大体わかると思うのですが、こちらにも使い方を書いておきます。

使い方

上記の shimabox/SMBBenchmark からソースを落としてください。
※Packagistへはそのうち、、、 composerの対応しました。

ソースの読み込み

・composer.json

{
  "require": {
    "shimabox/smbbenchmark": "^1.0"
  }
}

・SplClassLoaderを使う場合

require_once 'SplClassLoader.php';
$includePath = realpath(__DIR__ . '/../src');
$classLoader = new SplClassLoader('SMB', $includePath);
$classLoader->register();

・SplClassLoaderを使わない場合(手動とか)

require_once '/path/to/SMB/Benchmark.php';
require_once '/path/to/SMB/Benchmark/IFormatter.php';
require_once '/path/to/SMB/Benchmark/Formatter.php';

/SMB/Benchmark.php, /SMB/Benchmark/IFormatter.php, /SMB/Benchmark/Formatter.php が読み込めれば大丈夫です。

example.1 通常利用

// インスタンス生成
$bm = SMB\Benchmark::getInstance();

// スタート
$bm->start('bench1'); // 引数にマーキング用の値を渡す

// 計測処理
for ($i = 0; $i < 100; $i++) {
    new stdClass();
}

// 終了
$bm->end('bench1');

// 出力
// 小数点6桁(マイクロ秒単位)まで出力(デフォルト)
$bm->echoResult('bench1'); // => benchmark => bench1 : 0.・・・・・・秒

example.2 計測したい処理を無名関数内に書く(measure()を使う)

// インスタンス生成
$bm = SMB\Benchmark::getInstance();

// 計測したい処理をmeasure()に書く
// measure()はcallableを引数に取ります
$d = 4; // ローカル変数
$bm->measure(
    // 第1引数に無名関数も渡せてその中に処理も書ける
    function($a, $b, $c) use ($d) {
        echo $a . $b . $c . $d . PHP_EOL; // => 1234
    },
    array(1, 2, 3), // 関数に対して引数を配列で渡せる
    'bench2' // start(),end()を使わなくてもマークできる
);

$bm->echoResult('bench2');

// 関数の引数なしでマークありの場合は空の配列を第2引数に渡す
$bm->measure(
    function() {
        for ($i = 0; $i < 10000; $i++) {
            new stdClass();
        }
    },
    array(),
    'bench3'
);

// オブジェクトの関数も渡せる
require_once 'Hoge.php'; // サンプル用

$obj = new Hoge();

$bm->start('bench4');
$bm->measure(array($obj, 'callbackMethod'));
$bm->end('bench4');
$bm->echoResult('bench4');

// 引数は配列で渡す
$bm->start('bench5');
$bm->measure(array($obj, 'callbackMethod'), array(1, 2, 3));
$bm->end('bench5');
$bm->echoResult('bench5');

// 静的メソッドも測定可能
$bm->start('bench6');
$bm->measure(array('Hoge', 'staticCallbackMethod'));
$bm->end('bench6');
$bm->echoResult('bench6');

example.3 メソッドチェーン可能

$bm = SMB\Benchmark::getInstance()
        ->start('bench7')
        ->measure(function() {
            usleep(1000000); // 1秒
        })
        ->end('bench7')
        ->measure(function() {
            usleep(500000); // 0.5秒
        }, array(), 'bench8')
        ;

$bm->echoResult('bench7');
$bm->echoResult('bench8');

// 入れ子も可能
$bm = SMB\Benchmark::getInstance()
        ->start('bench9')
        ->start('bench10')
        ->measure(function() {
            usleep(100000); // 0.1秒
        })
        ->end('bench10')
        ->measure(function() {
            usleep(50000); // 0.05秒
        }, array(), 'bench11')
        ->end('bench9')
        ;

$bm->echoResult('bench9');  // => benchmark => bench9 : 0.15・・・・秒
$bm->echoResult('bench10'); // => benchmark => bench10 : 0.1・・・・・秒
$bm->echoResult('bench11'); // => benchmark => bench10 : 0.05・・・・秒

example.4 すべての計測結果を出力

$bm->echoResultAll(); // => bench1〜bench11までの計測結果が出力される

example.5 初期化

・上記でわかる通りシングルトンなので初期化用メソッドを持っています

// マーキングのクリア
$bm->clearMark('bench11');  // bench11をクリア
$bm->echoResult('bench11'); // => 出力なし
$bm->echoResultAll();       // => bench1〜bench10までの計測結果が出力される

// すべてのマーキングをクリア
$bm->clearMarkAll();
$bm->echoResultAll(); // => 出力なし

// インスタンスのクリア
SMB\Benchmark::clear();

example.6 ベンチマークの結果だけを返す

// 単独の出力
$bm = SMB\Benchmark::getInstance()
        ->measure(function() {
            usleep(50000); // 0.05秒
        }, array(), 'bench12')
        ->measure(function() {
            usleep(5000); // 0.005秒
        }, array(), 'bench13')
        ;

echo $bm->result('bench12') . PHP_EOL; // => 0.05・・・・
echo $bm->result('bench13') . PHP_EOL; // => 0.005・・・

// 計測結果すべての出力
$bm->clearMarkAll();
$bm = SMB\Benchmark::getInstance()
        ->start('bench14')
        ->start('bench15')
        ->measure(function() {
            usleep(100000); // 0.1秒
        })
        ->end('bench15')
        ->measure(function() {
            usleep(50000); // 0.05秒
        }, array(), 'bench16')
        ->end('bench14')
        ;

var_dump($bm->resultAll()); // => array('bench14'=>'0.15・・・・', 'bench15'=>'0.1・・・・・', 'bench16'=>'0.05・・・・')

$bm->clearMarkAll();
var_dump($bm->resultAll()); // => array()

その他

BCMath(任意精度数学関数)を使用しています

  • 浮動小数点を扱う為、BCMath(任意精度数学関数)を使用しています。
  • BCMathが使えなくても利用できますが測定結果の精度は落ちます。

2016/12/21 追記

BCMath(php-bcmath) のインストール方法

例です。各環境に合わせて修正してください。

環境

自分の環境はCentOS release 6.8 (Final)です

$ cat /etc/redhat-release # CentOS release 6.8 (Final)

インストール

# enablerepoは適宜修正
$ sudo yum install php-bcmath --enablerepo=remi,remi-php56

以下でインストールされているか確認できます

$ yum list installed php-bcmath
インストール済みパッケージ
php-bcmath.x86_64    5.6.29-1.el6.remi    @remi-php56

インストールできたらapacheのreloadを行います

$ sudo service httpd reload

2016/12/21 追記 ここまで

出力結果の小数点を変更

  • 出力結果はデフォルトで小数点6桁(0.000000秒 マイクロ秒単位)まで表示されていますが、これは変更可能です。
    • 小数点を少なくした場合、処理が早すぎるとほぼ0秒に丸められます
  • 変更するには setScale() を使います。
// インスタンス生成
$bm = SMB\Benchmark::getInstance();

// 小数点4桁に変更
$bm->setScale(4);

$bm->start('bench17');
for ($i = 0; $i < 1; $i++) {}
$bm->end('bench17');

// 出力

// 小数点4桁まで出力
$bm->echoResult('bench17'); // => benchmark => bench17 : 0.XXXX秒 (処理が早過ぎると、ほぼ0.0000秒になる)

// もちろんチェーン可能
SMB\Benchmark::getInstance()
    ->setScale(4)
    ->measure(function() {}, array(), 'bench18')
    ->echoResult('bench18')
    ;

// 1より小さい値をセットされたらデフォルトの6桁でセットし直します。
$bm->setScale(0); // => scaleは6になる

出力結果のフォーマットを変更

echoResult(), echoResultAll() の出力フォーマットはデフォルトで

benchmark => {$mark} : {$benchmark}秒

ですが、\SMB\Benchmark\Formatter::forEcho()を修正するか、\SMB\Benchmark\IFormatterを実装したクラスを作成しセットすることで好きなフォーマットに変更可能です。

・こんなフォーマッターを作成したら、

<?php

class SampleFormatter implements SMB\Benchmark\IFormatter
{
	public function forEcho($mark, $benchmark)
	{
		return '<pre>'.$mark.'の計測時間は'.$benchmark.'秒でした</pre>'.PHP_EOL;
	}
}

この様に使えます。

require_once 'SampleFormatter.php'; // サンプル用

$formatter = new SampleFormatter();
SMB\Benchmark::getInstance()
    ->setFormatter($formatter)
    ->measure(function() {}, array(), 'bench19')
    ->echoResult('bench19')
    ; // => bench19の計測時間は0.XXXXXX秒でした

PHP5.3対応版

・このライブラリの対象はPHP5.4以上ですが、PHP5.3でも動かしたい場合は

/example/php53/Benchmark.php

を利用してください。

違いは

  • Benchmark::measure() 第1引数のタイプヒンティング(callable)を削除
  • 無名関数内で$thisを使うために
    • Benchmark::resultAll(), Benchmark::echoResultAll() で$thisを退避
    • Benchmark::existsMark(), Benchmark::calc(), Benchmark::getFormatter() のアクセス修飾子をpublicに

です。(本当は配列もせっかくだから[]で扱いたかった。。)

Test

  • 簡単ではありますが、テストコードもあります
  • PHPUnitのバージョンは4.2.0で確認しています
phpunit --group SMBBenchmark
 or
phpunit

おわりに

こんな感じです。

最初作ってみたら短い処理の測定の時、浮動小数点の計算がわけわからん結果になっていて、浮動小数点だしこんなもんなのかなぁ。。と諦めていたのですが、マニュアルを見たところ

浮動小数点数の精度

浮動小数点数の精度は有限です。 システムに依存しますが、PHP は通常 IEEE 754 倍精度フォーマットを使います。 この形式は、1.11e-16 のオーダーでの丸め処理で誤差が発生します。 複雑な算術演算をすると、誤差はさらに大きくなるでしょう。そしてもちろん、 いくつかの演算を組み合わせる場合にも誤差を考慮しなければなりません。
さらに、十進数では正確な小数で表せる有理数、たとえば 0.1 や 0.7 は、 二進数の浮動小数点数としては正確に表現できません。 これは、仮数部をいくら大きくしても同じです。 したがって、それを内部的な二進数表現に変換する際には、どうしても多少精度が落ちてしまいます。 その結果、不思議な結果を引き起こすことがあります。たとえば、 floor((0.1+0.7)*10) の結果はたいてい 7 となるでしょう。おそらくは 8 を想定していらっしゃるでしょうが、そのようにはなりません。 これは、(この計算結果の) 内部的な値が 7.9999999999999991118… のようになっているからです。
よって、小数の最後の桁を信用してはいけませんし、 小数を直接比較して等しいかどうかを調べてはいけません。より高い精度が必要な場合には、 任意精度数学関数または gmp 関数を代わりに使用してください。

こんなのを見つけて、BC Math(任意精度数学関数)に出会ったわけです。
自分が試した環境(MAMPとかchefで構築した環境)だと、インストールを意識したつもりは無くても使えたのですが、もし使えなかったらすいません。

あと自分で使ってみた感想は、チェーンメソッドで書けるとか、マーキングしておいて後で出力出来る部分とか、無名関数に処理を書けるのは地味に便利かなと思いました。(探したらこんなの腐るほどありそうだけど)
時間があれば出力結果を受け取って可視化する様なのも作ろうかと思います。

最後に、このライブラリで出した計測結果はあくまでも目安として使って頂ければと思います。

参考にさせて頂きました

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください