$v === (bool)$v
ちょっと興味深いので比較してみました
条件の差を縮めるため、以下の式をラップしたユーザー定義関数の呼び出しを使います。
| label | expression |
|---|---|
| is_bool (\native) | \is_bool($v) |
| is_bool (ns) | is_bool($v) |
| in_array (\native) | \in_array($v, [true, false], true) |
| in_array (ns) | in_array($v, [true, false], true) |
| is_bool_cast | $v === (bool)$v |
| logical OR | $v === true || $v === false |
| match_expression | match ($v) { true, false => true, default => false} |
ここまで出てきた例以外にも、match式を加えてあります。
function is_bool_match(mixed $v): bool {
return match ($v) {
true, false => true,
default => false,
};
}is_bool() \is_bool() と in_array() \in_array()を別に計測するのは、後者のように書くことでZendVMで専用命令にコンパイルされるからです。
対象として、以下の3種類のデータセットを用います。
- bool型の値だけが含まれる (すべての結果は
true) - bool型の値は含まれない (すべての結果は
false) - boolと非boolが均等に含まれる (
true/falseが同一回数)- これに関しては各型やオブジェクトの種類にも影響しそうな気がします
ベンチマークコード
今回は説明を省略しますが、コード中に「名前空間解決による DO_FCALL」と書かれているのは、具体的には INIT_NS_FCALL_BY_NAME という命令です。
<?php
namespace Bench;
/**
* 1. ZEND_IS_BOOL 命令にコンパイルされる (最速期待)
*/
function is_bool_native(mixed $v): bool { return \is_bool($v); }
/**
* 2. 名前空間解決による DO_FCALL。実行時にグローバル関数を探す
*/
function is_bool_ns(mixed $v): bool { return is_bool($v); }
/**
* 3. ZEND_IN_ARRAY 命令にコンパイルされる (O(1) 最適化)
*/
function in_array_native(mixed $v): bool { return \in_array($v, [true, false], true); }
/**
* 4. 名前空間解決 + DO_FCALL + 通常の線形探索 (O(n))
*/
function in_array_ns(mixed $v): bool { return in_array($v, [true, false], true); }
/**
* 5. ユーザー定義ロジック:キャストと比較
*/
function is_bool_cast(mixed $v): bool { return $v === (bool)$v; }
/**
* 6. ユーザー定義ロジック:論理和
*/
function is_bool_or(mixed $v): bool { return $v === true || $v === false; }
/**
* 7. match式
*/
function is_bool_match(mixed $v): bool {
return match ($v) {
true, false => true,
default => false,
};
}
/**
* データセット
*/
$datasets = [
'all_bool' => [true, false, true, false],
'no_bool' => [1, 0, "1", "0", "true", [], new \stdClass(), null],
'half_half' => [true, 1, false, "test", true, null, false, []],
];
$testcases = [
'is_bool (\native)' => is_bool_native(...),
'is_bool (ns)' => is_bool_ns(...),
'in_array (\native)' => in_array_native(...),
'in_array (ns)' => in_array_ns(...),
'is_bool_cast' => is_bool_cast(...),
'logical OR' => is_bool_or(...),
'match_expression' => is_bool_match(...),
];
$iterations = 100000;
echo "PHP Version: " . PHP_VERSION . "\n";
echo str_repeat('=', 60) . "\n";
foreach ($datasets as $data_name => $values) {
echo "\nDataset: $data_name\n";
echo str_repeat('-', 60) . "\n";
$results = [];
foreach ($testcases as $name => $test) {
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
foreach ($values as $value) {
$test($value);
}
}
$end = hrtime(true);
// 合計実行回数で割って平均を出すのではなく、あえて「このデータセット一式の処理時間」とする
$results[$name] = ($end - $start) / 1e+6;
}
asort($results);
foreach ($results as $name => $ms) {
printf("%-20s : %10.4f ms\n", $name, $ms);
}
}ちゃんとしたベンチマーク用環境ではないのですが、3v4lを使ってみます。
PHP Version: 8.5.1
============================================================
Dataset: all_bool
------------------------------------------------------------
match_expression : 9.7004 ms
is_bool_cast : 9.7073 ms
logical OR : 9.8002 ms
in_array (ns) : 11.9967 ms
in_array (\native) : 17.0912 ms
is_bool (\native) : 22.0454 ms
is_bool (ns) : 27.1532 ms
Dataset: no_bool
------------------------------------------------------------
is_bool (\native) : 16.5042 ms
logical OR : 19.0521 ms
is_bool_cast : 19.4749 ms
match_expression : 19.7409 ms
in_array (\native) : 21.9979 ms
is_bool (ns) : 23.0111 ms
in_array (ns) : 24.1037 ms
Dataset: half_half
------------------------------------------------------------
is_bool (\native) : 16.8284 ms
match_expression : 19.0931 ms
is_bool_cast : 19.4886 ms
logical OR : 19.5219 ms
in_array (\native) : 21.8653 ms
is_bool (ns) : 23.0958 ms
in_array (ns) : 23.6194 ms
- 常にboolだけが入力される場合、
match_expressionis_bool_castlogical ORが最速ということになりました- 複数回実行すると、これらの順番は入れ替わります
- bool以外の場合が混ざる場合は、普通に
is_bool (\native)が最速で、それ以外はさっきの3兄弟が横並びになります- それ以外の選択肢も遅いといえば遅いのですが、
「全てboolのデータセットに対してis_bool (\native) が2倍遅い」というのは衝撃的な集計結果に見えるかもしれませんが、全体の実行時間に対して実行時間のオーダーが変わらないなら読みやすく簡潔、意味が明瞭になるように書くのが一番ましです。
特に業務プロジェクトで書かれているときは、どちらが意味が明瞭かということを考えるとよいでしょう。
どちらが「$v は bool である」という意図を明確に主張できるか
+assert(\is_bool($v));
-assert($v === (bool)$v);また、これらはif ()やassert()の中に直接比較式を書くことを想定した比較方法なので、「is_bool()よりも速いんだ!」と考えて is_bool() の利用箇所を function isBoolCast(mixed $v): bool { return $v === (bool)$v; } みたいなものに置き換えると、関数呼び出しのオーバーヘッドが毎回かかります。
パフォーマンスチューニングをするという観点では「なんとなく早そう」なものをコーディングしながら毎回悩む、あるいは複雑な式を書くよりは、短くわかりやすいものを選んで、処理時間を律速する要素を選んで改善していくのが良いでしょう。
Inspired by PHPを少しでも速く動かしたい #PHP - Qiita.