[PHP][速度測定] 関数に値を引数で渡す/＄GLOBALSで渡す/globalで渡す場合の速度の違い

> PHP では関数に値を渡す場合、以下の大きく３つの方法があります。各々の速度の違いが知りたいのです。

1. 引数で渡す（[arguments](http://php.net/manual/ja/functions.arguments.php)）

    ```php
    function myFunc($val)
    {
        echo $val;
    }
    ```

2. `$GLOBALS` 配列経由で渡す（[globals](http://php.net/manual/ja/reserved.variables.globals.php)）

    ```php
    $GLOBALS['foo']='bar';
    function myFunc($val)
    {
        $val=$GLOBALS['hoo'];
        echo $val;
    }
    ```

3. `global` 宣言経由で渡す（[variables scope](http://php.net/manual/ja/language.variables.scope.php)）

    ```php
    $val='bar';
    function myFunc($val)
    {
        globals $val;
        echo $val;
    }
    ```

速度やメモリ使用量を考えた場合、**PHP7 では、どの方法が軽いのか気になった**ので、MacBookPro と RaspberryPi3 で測定してみました。

[「関数 引数で渡す Globalsで渡す 速度」を Qiita に絞ってググって](https://www.google.co.jp/search?q=site%3Aqiita.com+php+%E9%96%A2%E6%95%B0+%E5%BC%95%E6%95%B0%E3%81%A7%E6%B8%A1%E3%81%99+Globals%E3%81%A7%E6%B8%A1%E3%81%99+%E9%80%9F%E5%BA%A6)もドンピシャの情報が出てこなかったので、測定したことすら忘れてしまう未来の自分への備忘録として。

## TL;DR（推測するなら、計測しろ）

子曰く

> 『パフォーマンスチューニングの鉄則は **推測するな、計測せよ** である。』
> （[パフォーマンス](https://qiita.com/tadsan/items/157969b338fd8b782b21#%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9) |「モダンPHPアンチパターン」 @ Qiita より）

### 検証結果

PHP7.x + MacBookPro/RaspberryPi3+ では以下の結果となりました。

> PHP7 では 100Mバイトのデータを100万回ぶん回しても 0.0n 秒の差。
> ∴ **「違いはほとんどない」ので、可読性やメンテナンス性を重視した方がよいレベル**

- 「引数渡し」 >= 「`global` 宣言渡し」 > 「`$GLOBALS` 配列渡し」（左から速度が速い順）
- 「引数渡し」と「`global` 宣言渡し」には、差は余りない
- メモリの使用量は、いずれも余り違いはない

### 総括

> （実際のアプリケーションではほとんどありえない前提での微々たる違いならば）
> 『私の結論としては、「書きやすいように（あるいは、事故を起こしにくいように）好きに書けばよい」です。』 （[コメントより](#comment-28565bb0d7a36b5e790b)）

実は 「引数渡し = `$GLOBALS` 配列渡し >>> `global` 宣言渡し」という、「`global` 宣言渡しは圧倒的に遅い」といった [StackOverflow の情報](https://stackoverflow.com/a/22002443/8367711)もあったので、最初は鵜呑みにしそうになりました。

しかし、他の情報のコメントを見ても議論が熱い割には噛み合っておらず、どの **PHP のバージョンを語っていないことが原因**と気づきました。

どうやら「`$GLOBALS` 配列渡し」が「`global` 宣言渡し」より速いというのは PHP5 時代の話らしく、PHP7 への移行の過渡期にある現在（2018/11）、**バージョン情報や日付などのない情報には、やはり測定してみるものだ**と思いました。（ってか PHP7 速いなー）

### 合わせて読みたい

> ボトルネックではないところの実行効率を上げても全体の最適化には繋がりません
> 
> - [PHPの長文テキストを書く時はどれでやる？>>>連結 or Enter改行 or ヒアドキュメント？](https://qiita.com/HorikawaTokiya/items/06cecc3317dcd39d59de#comment-28565bb0d7a36b5e790b) @ Qiita

## TS;DR（変なとろこで細かいことが気になりすぎて伝わらない人の検証内容）

### 検証条件

```php:検証条件
$num_iterate = 1000000; //１回の for ループ回数（100万回）
$num_loop    = 100; //平均を取るための回数（100万回の合計/100回）
$var_base    = str_repeat('x', 100000000); // 95.37 Mバイト
```

<details><summary>テストコード（全文）</summary><div>

処理内容の詳細は TS;DR をご覧ください。

```php:bench_globals.php
<?php

// 値を関数へ引数渡し, $GLOBALS渡し, global宣言渡しする場合の違いの速度テスト

$var_base = str_repeat('x', 100000000);
$GLOBALS['var_global'] = $var_base;

function testfunc_param($var_param)
{
    $var_local = $var_param;
    return $var_local;
}

function testfunc_global()
{
    global $var_global;
    $var_local = $var_global;
    return $var_local;
}

function testfunc_globalsarray()
{
    $var_local = $GLOBALS['var_global'];
    return $var_local;
}

function getValue($key, $array, $default = 0)
{
    return isset($array[$key]) ? $array[$key] : $default;
}

function echoResults($title, $time = null, $memory = null)
{
    static $results;

    if (null === $time) {
        print("{$title}\n\n");
        foreach ($results as $title => $result) {
            $ave_count  = getValue('count', $result);
            $ave_time   = getValue('total_time', $result) / $ave_count;
            $ave_memory = getValue('total_mem', $result) / $ave_count;
            print("{$title}\nTime: {$ave_time}sec\nMemory: {$ave_memory}\n\n");
        }

        exit(0);
    }

    $result_each = getValue($title, $results, array());

    $results[$title] = [
        'count'      => getValue('count', $result_each) + 1,
        'total_time' => getValue('total_time', $result_each) + $time,
        'total_mem'  => getValue('total_mem', $result_each) + $memory,
    ];

    print("{$title}\nTime: {$time} sec\nMemory: {$memory}\n\n");
}


$num_loop    = 100;
$num_iterate = 1000000;

for ($loop = 0; $loop < $num_loop; $loop++) {

    memory_get_usage();
    $memoryStart = memory_get_usage();
    $timeStart   = microtime(true);
    for ($i = 0; $i < $num_iterate; $i++) {
        testfunc_param($var_base);
    }
    $timeEnd   = microtime(true);
    $memoryEnd = memory_get_peak_usage();
    echoResults(
        'Pass value by parameter',
        $timeEnd - $timeStart,
        $memoryEnd - $memoryStart
    );

    memory_get_usage();
    $memoryStart = memory_get_usage();
    $timeStart   = microtime(true);
    for ($i = 0; $i < $num_iterate; $i++) {
        testfunc_global();
    }
    $timeEnd   = microtime(true);
    $memoryEnd = memory_get_peak_usage();
    echoResults(
        'Global var reference',
        $timeEnd - $timeStart,
        $memoryEnd - $memoryStart
    );

    memory_get_usage();
    $memoryStart = memory_get_usage();
    $timeStart   = microtime(true);
    for ($i = 0; $i < $num_iterate; $i++) {
        testfunc_globalsarray();
    }
    $timeEnd   = microtime(true);
    $memoryEnd = memory_get_peak_usage();
    echoResults(
        'GLOBALS array reference',
        $timeEnd - $timeStart,
        $memoryEnd - $memoryStart
    );
}

echoResults('* Total Average:');

```

</div></details>

- [オンラインで動作を見る](https://paiza.io/projects/djUgQQcH5kcuWsRKGEauzw) @ paiza.IO

<details><summary>検証環境：macOS HighSierra + PHP 7.2.6</summary><div>

```php:検証環境
$ date && sw_vers && system_profiler -detailLevel mini SPHardwareDataType && php -version
2018年 11月14日 水曜日 13時36分13秒 JST
ProductName:	Mac OS X
ProductVersion:	10.13.6
BuildVersion:	17G3025
Hardware:

    Hardware Overview:

      Model Name: MacBook Pro
      Model Identifier: MacBookPro12,1
      Processor Name: Intel Core i5
      Processor Speed: 2.7 GHz
      Number of Processors: 1
      Total Number of Cores: 2
      L2 Cache (per Core): 256 KB
      L3 Cache: 3 MB
      Memory: 8 GB
      Boot ROM Version: 180.0.0.0.0
      SMC Version (system): 2.28f7

PHP 7.2.6 (cli) (built: May 25 2018 06:18:43) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.6, Copyright (c) 1999-2018, by Zend Technologies
```

</div></details>

<details><summary>検証環境：Raspbian Jessie + PHP 7.1.12</summary><div>

```php:検証環境
$ echo && date && cat /etc/os-release | head -1 && cat /proc/cpuinfo | grep -e "model name" -e "Hardware" -e "Revision" | uniq && cat /proc/meminfo |egrep -e "Active:|Inactive:|MemFree:" && php -v | head -1

2018年 11月 14日 水曜日 14:12:21 JST
PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
model name	: ARMv7 Processor rev 4 (v7l)
Hardware	: BCM2835
Revision	: a22082
MemFree:          157004 kB
Active:           438324 kB
Inactive:         275852 kB
PHP 7.1.12-1+0~20171129100725.11+jessie~1.gbp8ded15 (cli) (built: Dec  1 2017 04:22:35) ( NTS )
```
</div></details>

### 測定結果

```php:結果@MacBookPro
* Total Average:

Pass value by parameter # 引数渡し（約 0.048 秒）
Time: 0.04791793346405 sec
Memory: 772.16

Global var reference # global 宣言渡し（約 0.053 秒）
Time: 0.05299122095108 sec
Memory: 779.12

GLOBALS array reference # $GLOBALS 配列渡し（約 0.066 秒）
Time: 0.066218328475952sec
Memory: 756.24
```

```php:測定結果@RPi3
* Total Average:

Pass value by parameter # 引数渡し（約 0.394 秒）
Time: 0.39396689653397sec
Memory: 646.48

Global var reference # global 宣言渡し（約 0.433 秒）
Time: 0.43290241241455sec
Memory: 652.48

GLOBALS array reference # $GLOBALS 配列渡し（約 0.479 秒）
Time: 0.47904109477997sec
Memory: 629.28
```
### コード解説

基本的に上記「検証条件」のテストコード（全文）と同じ内容ですが、少し噛み砕いて説明を知りたい方向け。

まずは 100 MB 近くの文字列を用意し、グローバル配列にセットします。

```php:検証に使うデータ
$var_base = str_repeat('x', 100000000); // 95.37MB
$GLOBALS['var_global'] = $var_base;
```

次に、「引数渡し」「`global` 宣言渡し」「`$GLOBALS` 配列渡し」の 3 つのテスト用関数を用意しました。

```php:検証に使うテスト関数
// 引数渡し
function testfunc_param($var_param)
{
    $var_local = $var_param;
    return $var_local;
}

// global 宣言渡し
function testfunc_global()
{
    global $var_global;
    $var_local = $var_global;
    return $var_local;
}

// $GLOBALS 配列渡し
function testfunc_globalsarray()
{
    $var_local = $GLOBALS['var_global'];
    return $var_local;
}
```

次に関数をループする回数と、平均を取るための繰り返しの回数を用意しました。

```php:ループ設定
$num_iterate = 1000000; //１回の for ループ回数（100万回）
$num_loop    = 100;     //平均を取るための回数（100万回の合計/100回）
```

１処理（１関数呼び出し）の、速度およびメモリ使用量の測定処理は以下の通り。

```php:１回ぶんの処理
memory_get_usage(); // 空の呼び出し。ガーベジコレクションされるのか、これをしないと安定して測定できなかった

$memoryStart = memory_get_usage(); //現在のメモリ量取得
$timeStart   = microtime(true);    //現在の時間を取得

for ($i = 0; $i < $num_iterate; $i++) {
    <テスト関数>(<$var_base>);
}

$timeEnd   = microtime(true);         //終了時間を取得
$memoryEnd = memory_get_peak_usage(); //終了時のメモリ量を取得

//１回ぶんの測定結果の記録と表示
echoResults(
    'Pass value by parameter', //表示タイトル
    $timeEnd - $timeStart,     //使用時間
    $memoryEnd - $memoryStart  //使用メモリ
);
```

上記を 3 つのテスト関数ぶん繰り返し、平均を取るためにループさせます。

```php:平均の測定
for ($loop = 0; $loop < $num_loop; $loop++) {
    // 引数渡しのストレス・テスト
    <$num_iterate ぶん繰り返し>

    // global 宣言渡しのストレス・テスト
    <$num_iterate ぶん繰り返し>

    // $GLOBALS 配列渡しのストレス・テスト
    <$num_iterate ぶん繰り返し>
}

// 測定結果の平均値の表示
echoResults('* Total Average:');
```

```php:echoResults()関数
function echoResults($title, $time = null, $memory = null)
{
    static $results;

    //平均の表示
    if (null === $time) {
        print("{$title}\n\n");
        foreach ($results as $title => $result) {
            $ave_count  = getValue('count', $result);
            $ave_time   = getValue('total_time', $result) / $ave_count;
            $ave_memory = getValue('total_mem', $result) / $ave_count;
            print("{$title}\nTime: {$ave_time}sec\nMemory: {$ave_memory}\n\n");
        }

        exit(0);
    }

    //処理結果の保存
    $result_each = getValue($title, $results, array());
    $results[$title] = [
        'count'      => getValue('count', $result_each) + 1,
        'total_time' => getValue('total_time', $result_each) + $time,
        'total_mem'  => getValue('total_mem', $result_each) + $memory,
    ];

    //１回ごとの処理結果の表示
    print("{$title}\nTime: {$time} sec\nMemory: {$memory}\n\n");
}
```

## 所感

**アプリの諸設定を色々な関数に渡して使いまわしたかった**のです。

ここで言う諸設定とは、ファイルやディレクトリのパスや、フラグなどのことです。

その場合、クラス・オブジェクトにするのが一般的なのかもしれませんが、アプリの規模自体が小さいため、配列に諸設定を突っ込んで、関数（の引数）に渡していました。

```php
functions myFunc1(array $settings)
{
  // Do something with $settings
}

functions myFunc2(array $settings)
{
  // Do something with $settings
}
```

途中から、ふと
「**毎回、関数に引数で渡すの面倒**だから、関数内で `global $settings;` としちゃってもいいんじゃね？」
と邪念がよぎったのですが、
「下手に楽をしてパフォーマンスが落ちたらどうしよう」
と、パフォーマンスもヘッタクレも関係ない小さなアプリに対して悩んでしまったのです。

[Qiita に絞ってググって](https://www.google.co.jp/search?q=site%3Aqiita.com+php+%E9%96%A2%E6%95%B0+%E5%BC%95%E6%95%B0%E3%81%A7%E6%B8%A1%E3%81%99+Globals%E3%81%A7%E6%B8%A1%E3%81%99+%E9%80%9F%E5%BA%A6)もピンポイントで出てこなかったので、[しぶしぶググった](https://www.google.co.jp/search?q=php+%E9%96%A2%E6%95%B0+%E5%BC%95%E6%95%B0%E3%81%A7%E6%B8%A1%E3%81%99+Globals%E3%81%A7%E6%B8%A1%E3%81%99+%E9%80%9F%E5%BA%A6)ところ、全体としては「気にするな」的な意見が多いのですが、「`$GLOBALS` の方が `global` で宣言するよりも速い」「いや `global` の方が速い」など、「C がそうだから」「理屈で考えてもそうだから」と、よく理解できない根拠を理由に**意見が分かれていました**。

「[パフォーマンスチューニングの鉄則は **推測するな、計測せよ**](https://qiita.com/tadsan/items/157969b338fd8b782b21#%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9)」という言葉を思い出していたところ、同じ議論を StackOverflow で見つけ[「実測しようよ」というコメントを見つけた](https://stackoverflow.com/questions/22001848/global-variable-or-pass-variable-in-php-performance)のです。

全体を読まずに「これは！」と[コピペピピック](https://qiitadon.com/@KEINOS/99692402654853280)しそうになったのですが、なんかそこでも「いや、俺の環境では違う」といったコメントがありました。どうやら PHP のバージョン違いっぽいです。

これはまさに「[実際の運用と近い環境とユースケース（データ・処理）に対して行ってください。そうしないと、実運用環境では生じない誤った結論に導かれることがあります](https://qiita.com/HorikawaTokiya/items/06cecc3317dcd39d59de#comment-28565bb0d7a36b5e790b)」に該当する内容です。

そこで、~~アプリの開発にも飽きてきたので~~いささか気になったので測定してみようと思いたったわけです。

ところが、測定結果のメモを取っている時点で「えっとー、どっちが速いんだっけ？」となったので、「これは未来の俺は絶対に忘れる」と気づき「記事にせねば」と思い記事にしました。どう？未来の自分、思い出した？

## 参考文献

- 「[Global variable or pass variable in PHP? (performance)](https://stackoverflow.com/questions/22001848/global-variable-or-pass-variable-in-php-performance)」@ StackOverflow
- 「[`memory_get_usage`](http://php.net/manual/ja/function.memory-get-usage.php)」@ PHP マニュアル
- 「[`memory_get_peak_usage`](http://php.net/manual/ja/function.memory-get-peak-usage.php`)」 @ PHP マニュアル