結論
関数呼び出し分のオーバーヘッドで遅い
環境情報
Windows11機から、WSL2でUbuntu20.04を立ち上げています。
CPU: 13th Gen Intel(R) Core(TM) i7-13700F
# php -v
]PHP 8.1.2-1ubuntu2.14 (cli) (built: Aug 18 2023 11:41:11) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.2, Copyright (c) Zend Technologies
with Zend OPcache v8.1.2-1ubuntu2.14, Copyright (c), by Zend Technologies
array_push()
は遅い
「PHP 配列 追加」と検索するとまず紹介されるのがarray_push()
ですが、これは遅すぎの王、遅すぎキングと有名ですね。
代わりに$(変数名)[] = (追加要素)
という文法を推奨されます。
実際に時間計測してみましょう。
test.php
<?php
$time_start_0 = microtime(true);
for ($cnt = 0; $cnt < 1000000; $cnt++){
$a = [1,2,3,4,5,6,7,8,9,10];
array_push($a, 11);
}
$time_0 = microtime(true) - $time_start_0;
var_dump($time_0);
$time_start_1 = microtime(true);
for ($cnt = 0; $cnt < 1000000; $cnt++){
$a = [1,2,3,4,5,6,7,8,9,10];
$a[] = 11;
}
$time_1 = microtime(true) - $time_start_1;
var_dump($time_1);
//何倍速くなったか
var_dump($time_0 / $time_1);
結果
# php test.php
float(0.03344011306762695)
float(0.021086931228637695)
float(1.5858216970998926)
1.5倍と出ました。人によっては3倍になるらしいですね
しかし、これは両者とも時間計算量はO(1)
とのことです。
なぜ?
phpdbg
で下記コードの中間コードを表示してみましょう。
test.php
<?php
$a = [];
array_push($a, 1);
$a[] = 2;
中間コード
# phpdbg test.php
prompt> print e
[Context /test.php (8 ops)]
$_main:
; (lines=8, args=0, vars=1, tmps=3)
; /test.php:1-8
L0003 0000 ASSIGN CV0($a) array(...)
L0005 0001 INIT_FCALL 2 112 string("array_push")
L0005 0002 SEND_REF CV0($a) 1
L0005 0003 SEND_VAL int(1) 2
L0005 0004 DO_ICALL
L0007 0005 ASSIGN_DIM CV0($a) NEXT
L0007 0006 OP_DATA int(2)
L0008 0007 RETURN int(1)
L0005
の中間コードを見てください。5行目はarray_push()
でしたね。
上から
-
array_push
という名前の関数を拾ってくる - 引数
$a
の参照を送信 - 引数
2
を送信 - 関数の処理を呼び出し(
DO_ICALL
)
という感じのニュアンスです。(厳密にはたぶん違います)
L0007
は$a[] = 2
のコードですが、 実は これはL0005 0004
のDO_ICALL
と同内容です
つまり、array_push()
は 関数呼び出しの分だけ 無駄な処理を行っているわけです。
一方、$a[] = 2
はオペレーターに処理を埋め込まれている(?)ので、 余計な処理がない ということですね。
このような標準関数の中間コード変換問題については、こちらの記事が面白いです:ZendEngineにえこひいきされる標準関数たち (前篇)