[php] 配列から Iterator を作る最も効率のいい方法

# やりたいこと

- [配列](https://www.php.net/manual/ja/language.types.array.php)から [Iterator](https://www.php.net/manual/ja/class.iterator.php) を作りたい。
- なるべくはやく動いてほしい。
- [ArrayIterator](https://www.php.net/manual/ja/class.arrayiterator.php) ってあるけど、それってホントに早いの？
- php 5.6.10 と php 7.4.3 で同じコードで確かめました。

# 準備

まずは準備するためのコード。

## 配列

てきとーに大きい連想配列を作ります。
楽なので key も value も `int` にしました。
ところで、 `PHP_INT_MIN` って、 php 5 にはないんですね。

```php
if (!defined('PHP_INT_MIN')) {
    define('PHP_INT_MIN', -PHP_INT_MAX - 1);
}

function create_array() {
    $array = [];
    while (count($array) < 1000000) {
        $array[mt_rand(PHP_INT_MIN, PHP_INT_MAX)] = mt_rand(PHP_INT_MIN, PHP_INT_MAX);
    }
    return $array;
}

$array = create_array();
echo '$array length: ' . count($array) . PHP_EOL;
echo PHP_EOL;
```

## テスト関数

テスト関数はこんな感じ。
key も value も両方使いたかったのでそれらをひたすら加算するもの。
30 回計測して 10 回ごとに結果を出力します。

```php
function doTest(array $array, callable $iterator_getter) {
    $times = [];
    for ($i = 0; $i < 3; ++$i) {
        $start = microtime(true);
        $sum = 0;
        for ($j = 0; $j < 10; ++$j) {
            foreach ($iterator_getter($array) as $key => $value) {
                $sum += $key;
                $sum += $value;
            }
        }
        $time = microtime(true) - $start;
        $times[] = $time;
        echo 'time: ' . $time . ', sum: ' . $sum . PHP_EOL;
    }
    echo 'avg: ' . (array_sum($times) / count($times)) . PHP_EOL;
    echo PHP_EOL;
}
```

# テストコード

遅かった順に紹介していきます。
結果は後でまとめて。

## テスト 1 内部ポインタ

ワースト 1 はこちら！
この次のやつより早いと思ったのに期待外れ。
内部ポインタ関数を使って Iterator を実装してみました。

内部ポインタ関数には、 [`reset`](https://www.php.net/manual/ja/function.reset.php), [`end`](https://www.php.net/manual/ja/function.end.php), [`next`](https://www.php.net/manual/ja/function.next.php), [`prev`](https://www.php.net/manual/ja/function.prev.php), [`current`](https://www.php.net/manual/ja/function.current.php), [`key`](https://www.php.net/manual/ja/function.key.php), [`each`](https://www.php.net/manual/ja/function.each.php) があります。
そのうち、[`reset`](https://www.php.net/manual/ja/function.reset.php), [`next`](https://www.php.net/manual/ja/function.next.php), [`key`](https://www.php.net/manual/ja/function.key.php), [`current`](https://www.php.net/manual/ja/function.current.php) を使います。

### コード 1-1

```php
echo 'test 1-1: InternalPointerArrayIterator' . PHP_EOL;
class InternalPointerArrayIterator implements Iterator {
    private $array;
    public function __construct($array) { $this->array = $array; }
    public function rewind()            { reset($this->array); }
    public function next()              { next($this->array); }
    public function valid()             { return key($this->array) !== null; }
    public function current()           { return current($this->array); }
    public function key()               { return key($this->array); }
}

doTest($array, function ($array) { return new InternalPointerArrayIterator($array); });
```

## テスト 2 index

今度は index を使ってみます。
[`array_values`](https://www.php.net/manual/ja/function.array-values.php) と [`array_keys`](https://www.php.net/manual/ja/function.array-keys.php) を使って連想配列を index - key - value に分けるワザです。

これらを使えば、添え字が 0 から始まる共通の index を持つ配列を得られます。
`array_values` は index => value となり
`array_keys` は index => key となります。

index => [ key, value ] ってなる関数はないんですね。。。

ちなみに [`array_combine`](https://www.php.net/manual/ja/function.array-combine.php) は key にしたい配列と value にしたい配列を組み合わせる関数だそうです。

### コード 2-1 キーを使う版

```php
echo 'test 2-1: ArrayIndexKeyValueIterator' . PHP_EOL;
class ArrayIndexKeyValueIterator implements Iterator {
    private $index;
    private $keys;
    private $values;
    public function __construct($array) {
        $this->keys = array_keys($array);
        $this->values = array_values($array);
    }
    public function rewind()  { $this->index = 0; }
    public function next()    { ++$this->index; }
    public function valid()   { return $this->index < count($this->keys); }
    public function current() { return $this->values[$this->index]; }
    public function key()     { return $this->keys[$this->index]; }
}

doTest($array, function test2_1($array) { return new ArrayIndexKeyValueIterator($array); });
```

### コード 2-2 キーを使わない版。

[`array_keys`](https://www.php.net/manual/ja/function.array-keys.php) を使わずに `key` メソッドの実装では index を返すようにします。

```php
echo 'test 2-2 ArrayIndexValueIterator' . PHP_EOL;
class ArrayIndexValueIterator implements Iterator {
    private $index;
    private $values;
    public function __construct($array) {
        $this->values = array_values($array);
    }
    public function rewind()  { $this->index = 0; }
    public function next()    { ++$this->index; }
    public function valid()   { return $this->index < count($this->values); }
    public function current() { return $this->values[$this->index]; }
    public function key()     { return $this->index; }
}

doTest($array, function ($array) { return new ArrayIndexValueIterator($array); });
```

## テスト 3 ArrayIterator

[`ArrayIterator`](https://www.php.net/manual/ja/class.arrayiterator.php) は配列を抱えて Iterator のように振る舞うクラスです。
stdClass などのオブジェクトも抱えることができます。
[`ArrayObject`](https://www.php.net/manual/ja/class.arrayobject.php) の子供です。

### コード 3-1 キーを使う版

```php
echo 'test 3-1: ArrayIterator' . PHP_EOL;
doTest($array, function ($array) { return new ArrayIterator($array); });
```

## テスト 4 ジェネレーター

これをやりたかったから今までのテストがあるんです。

[`yield`](https://www.php.net/manual/ja/language.generators.php) を使う関数は [`Generator`](https://www.php.net/manual/ja/class.generator.php) という [`Iterator`](https://www.php.net/manual/ja/class.iterator.php) を実装したクラスを返します。

### コード 4-1 key 使う版

```php
echo 'test 4-1: yield key value' . PHP_EOL;
doTest($array, function ($array) { foreach ($array as $key => $value) { yield $key => $value; } });
```

### コード 4-2 key 使わない版

```php
echo 'test 4-2: yield value' . PHP_EOL;
doTest($array, function ($array) { foreach ($array as $value) { yield $value; } });
```

#テスト結果

#### php5

```shell
$array length: 1000000

test 1-1: InternalPointerArrayIterator
time: 9.3887410163879, sum: -1.6109817797916E+16
time: 9.4301509857178, sum: -1.6109817797916E+16
time: 9.4480760097504, sum: -1.6109817797916E+16
avg: 9.4223226706187

test 2-1: ArrayIndexKeyValueIterator
time: 7.8769841194153, sum: -1.6109817797916E+16
time: 7.2404820919037, sum: -1.6109817797916E+16
time: 7.5442090034485, sum: -1.6109817797916E+16
avg: 7.5538917382558

test 2-2 ArrayIndexValueIterator
time: 6.0075109004974, sum: -5.3709754974784E+15
time: 5.8148491382599, sum: -5.3709754974784E+15
time: 5.8173658847809, sum: -5.3709754974784E+15
avg: 5.8799086411794

test 3-1: ArrayIterator
time: 3.4061341285706, sum: -1.6109817797916E+16
time: 3.4112520217896, sum: -1.6109817797916E+16
time: 3.4074258804321, sum: -1.6109817797916E+16
avg: 3.4082706769307

test 4-1: yield key value
time: 3.8069369792938, sum: -1.6109817797916E+16
time: 3.8960921764374, sum: -1.6109817797916E+16
time: 3.8957939147949, sum: -1.6109817797916E+16
avg: 3.866274356842

test 4-2 yield value
time: 3.8193328380585, sum: -5.3709754974784E+15
time: 3.8335611820221, sum: -5.3709754974784E+15
time: 3.8304271697998, sum: -5.3709754974784E+15
avg: 3.8277737299601
```

#### php7

```shell
$array length: 1000000

test 1-1: InternalPointerArrayIterator
time: 2.9903738498688, sum: 3.3884535088043E+22
time: 2.9772939682007, sum: 3.3884535088043E+22
time: 2.9829080104828, sum: 3.3884535088043E+22
avg: 2.9835252761841

test 2-1: ArrayIndexKeyValueIterator
time: 2.3398787975311, sum: 3.3884535088043E+22
time: 2.325747013092, sum: 3.3884535088043E+22
time: 2.3066658973694, sum: 3.3884535088043E+22
avg: 2.3240972359975

test 2-2 ArrayIndexValueIterator
time: 2.1043679714203, sum: -6.4247619686786E+21
time: 2.0666069984436, sum: -6.4247619686786E+21
time: 2.0694139003754, sum: -6.4247619686786E+21
avg: 2.0801296234131

test 3-1: ArrayIterator
time: 0.77759599685669, sum: 3.3884535088043E+22
time: 0.7790789604187, sum: 3.3884535088043E+22
time: 0.77693104743958, sum: 3.3884535088043E+22
avg: 0.77786866823832

test 4-1: yield key value
time: 0.61654806137085, sum: 3.3884535088043E+22
time: 0.60638499259949, sum: 3.3884535088043E+22
time: 0.60806393623352, sum: 3.3884535088043E+22
avg: 0.61033233006795

test 4-2: yield value
time: 0.56992816925049, sum: -6.4247619686786E+21
time: 0.5589599609375, sum: -6.4247619686786E+21
time: 0.56230807304382, sum: -6.4247619686786E+21
avg: 0.56373206774394
```

| No | 項目 | key | php5 | php7 |
|:-:|:-:|:-:|:-:|:-:|
| 1-1 | 内部ポインタ | あり | 9.4223226706187 | 2.9835252761841 |
| 2-1 | index  | あり| 7.5538917382558 | 2.3240972359975 |
| 2-2 | | なし | 5.8799086411794 | 2.0801296234131 |
| 3-1 | ArrayIterator | あり  | 3.4082706769307 | 0.77786866823832 |
| 4-1 | ジェネレーター | あり | 3.866274356842 | 0.61033233006795 |
| 4-2 | | なし | 3.8277737299601 | 0.56373206774394 |

# まとめ

- もう php7 優秀すぎ。まったく同じコードなのにこの差。約 3 倍。
- 与えられたものつかっとけ。無理に作るな。
- キーを使わないようにケチってもそれほど差は得られない。
- ほかにもっといい方法、早くなくても面白い方法あれば教えてください。