【Laravel12】Arr::dot()メソッドが爆速化しました

Laravel

日本時間2025/04/22深夜にリリースされた Laravel v12.10.0 に盛り込まれた改善点の一つとして、Arr::dot() メソッドの爆速化があります。

Improve performance of Arr::dot method - 300x in some cases by cyppe ?? Pull Request #55495 ?? laravel/framework
Array::dot Method Performance OptimizationOverviewThis PR optimizes the Arr::dot() method, replacing the recursive imple...

ベンチマークの結果、最大300倍の速度改善が見られたようです。

ちょっと見ていきましょう。

プルリクの内容

Improve performance of Arr::dot method - 300x in some cases by cyppe ?? Pull Request #55495 ?? laravel/framework
Array::dot Method Performance OptimizationOverviewThis PR optimizes the Arr::dot() method, replacing the recursive imple...

▼以下、自動翻訳に少し手を入れたものです。

概要

このPRは、Arr::dot()メソッドを最適化し、再帰的実装をClosureベースの反復アプローチに置き換えます。最適化により、大きなネストされた配列を平坦化するとパフォーマンスが大幅に向上します。これは、大規模なデータセットを処理する際のLaravelのバリデーターにとって特に有益です。

最適化の詳細

元の実装では、再帰的に array_merge() の呼出を使用しました。これは、大きな配列のO(n²)動作につながる可能性があります:

public static function dot($array, $prepend = '')
{
    $results = [];

    foreach ($array as $key => $value) {
        if (is_array($value) && ! empty($value)) {
            $results = array_merge($results, static::dot($value, $prepend.$key.'.'));
        } else {
            $results[$prepend.$key] = $value;
        }
    }

    return $results;
}

最適化された実装では、リファレンスを備えたネストされたClosureを使用して、コストの高い再帰的なarray_merge()呼出を回避します。

public static function dot($array, $prepend = '')
{
    $results = [];

    $flatten = function ($data, $prefix) use (&$results, &$flatten): void {
        foreach ($data as $key => $value) {
            $newKey = $prefix.$key;

            if (is_array($value) && ! empty($value)) {
                $flatten($value, $newKey.'.');
            } else {
                $results[$newKey] = $value;
            }
        }
    };

    $flatten($array, $prepend);

    return $results;
}

ベンチマーク結果

ベンチマークは、さまざまなサイズの配列で実行され、実際の検証シナリオをシミュレートしました。各ベンチマークは5回の反復を実行し、平均実行時間を記録しました。

テスト環境

  • PHP Version: 8.3.20
  • Laravel Framework Version: v12.9.2
  • Machine: MacBook Pro (M-series chip)

テストデータの構造

ベンチマークは、Laravelのバリデーターによって処理されたものと同様のネストされた配列を使用しました。

[
    'items' => [
        [
            'id' => 0,
            'name' => 'Name 0',
            'email' => 'email0@example.com',
            'date' => '2023-05-20',
            'another_sub_array' => [
                'street' => 'Street 0',
                'city' => 'City 0',
                'country' => 'Country 0',
            ],
        ],
        // ... more items
    ]
]

パフォーマンス比較

配列
サイズ
(要素数)
元の実装改善した実装改善率速度
上昇率
5005.2261 ms0.4425 ms91.5%11.8倍
1,00020.2319 ms0.6762 ms96.7%30倍
5,000556.6993 ms3.6952 ms99.3%150倍
10,0003,591.2605 ms10.6625 ms99.7%337倍

メモリ消費

メモリ消費は、元の実装と最適化された実装の間で一貫していたままで、メモリのオーバーヘッドは追加されていません。

配列サイズ(要素数)メモリ消費
5000.41 MB
1,0000.65 MB
5,0004.21 MB
10,0008.43 MB

大きなデータセットへの影響

パフォーマンスの改善は、大規模なデータセットで特に重要です。ベンチマークで実証されているように、10,000項目の配列の処理は3.5秒以上からわずか10.6ミリ秒に改善されました。これは99.7%の改善です。

この最適化は、ワイルドカードルールを使用して大規模なデータセットを検証する際にユーザーが大幅に減速した #49375 で報告されたパフォーマンスの問題に対処します。

テスト方法

1.同じ入力データを使用して両方の実装を比較するためにベンチマークスクリプトが作成されました
2.各実装は、サイズの増加(500、1000、5000、および10000要素)の配列でテストされました
3.各テストは5回の反復を実行して、一貫した結果を確保しました
4.スクリプトは、両方の実装の実行時間とメモリ使用量を測定しました
5.すべてのコアLaravelテストは、最適化された実装で引き続き合格し続けます

結論

最適化されたArr::dot()メソッドは、同一の機能を維持しながら、大きな配列のパフォーマンスの大幅な改善を提供します。この最適化には、ドット表記を使用してネストされた配列、特に大規模なデータセットを処理する際のバリデーターで動作するLaravel機能に役立ちます。

変更は非破壊的、既存のAPIと完全に後方互換性があります。

パフォーマンスの検証

ベンチマークのテストコードが示されていなかったので、実際に手元で検証してみました。

\Illuminate\Support\Benchmark

の measure() メソッドを使っていきます。

作業フォルダをバージョン毎に用意します。

Laravel 12.10.0 の直前のバージョンは Laravel 12.9.2 です。

各バージョンフォルダでBenchmark::measure()を実行し、結果を比較します。

次のようなファイル構成です。

[作業フォルダ]
 ├─ 12.9.2/
 │  ├─ benchmark.php ・・・ 12.9.2のベンチマーク実行
 │  └─ vendor/
 ├─ 12.10.0/
 │  ├─ benchmark.php ・・・ 12.10.0のベンチマーク実行
 │  └─ vendor/
 ├─ benchmark_base.php ・・・ベンチマーク共通処理
 └─ compare.php ・・・結果比較処理

まずは作業フォルダの準備から。

各バージョンフォルダ内に該当バージョンの laravel/framework をインストールします。

ベンチマーク共通処理「benchmark_base.php」

各要素数毎に配列を事前準備してからBenchmark::measure()を実行し、結果の秒数をカンマ区切りで出力します。

<?php

use Illuminate\Support\Arr;
use Illuminate\Support\Benchmark;

function provideArray(int $size) {
    $array = ['items' => []];
    for ($i = 0; $i < $size; $i++) {
        $array['items'][] = [
            'id' => $i,
            'name' => "Name {$i}",
            'email' => "email{$i}@example.com",
            'date' => '2023-05-20',
            'another_sub_array' => [
                'street' => "Street {$i}",
                'city' => "City {$i}",
                'country' => "Country {$i}",
            ],
        ];
    }
    return $array;
}

$arrays = [
    500 => provideArray(500),
    1000 => provideArray(1000),
    5000 => provideArray(5000),
    10000 => provideArray(10000),
];

$results = Benchmark::measure([
    '500' => fn () => Arr::dot($arrays[500]),
    '1000' => fn () => Arr::dot($arrays[1000]),
    '5000' => fn () => Arr::dot($arrays[5000]),
    '10000' => fn () => Arr::dot($arrays[10000]),
], 5);

echo implode(',', $results);

各バージョン毎のベンチマーク実行処理:「benchmark.php」

※バージョン毎の「autoload.php」を取り込み、「benchmark_base.php」を取り込みます。

<?php

require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/../benchmark_base.php';

結果比較:「compare.php」(やっつけコードですみませぬ)

※各バージョンの実行結果をカンマ区切りで受け取り、explodeしてfloat化、

※比較計算結果をMarkdown形式の表として出力。

<?php

$results = [
    '12.10.0' => array_map(fn ($r) => (float) $r, explode(',', `php -f 12.10.0/benchmark.php`)),
    '12.9.2' => array_map(fn ($r) => (float) $r, explode(',', `php -f 12.9.2/benchmark.php`)),
];

$sizes = [500, 1000, 5000, 10000];
echo '|Size|Original|Optimized|Improvement|Speed Up|' . PHP_EOL;
echo '|---:|---:|---:|---:|---:|' . PHP_EOL;
foreach ($results['12.9.2'] as $key => $original) {
    $optimized = $results['12.10.0'][$key];
    echo '|' . implode(
        '|', [
        number_format($sizes[$key], 0, '.', ','),
        number_format($original, 4, '.', ',') . ' ms',
        number_format($optimized, 4, '.', ',') . ' ms',
        number_format(100 * ($original - $optimized) / $original, 1, '.', ',') . '%',
        number_format($original / $optimized, 1, '.', ',') . 'x'
    ]) . '|' . PHP_EOL;
}

PHP 8.2 / 8.3 / 8.4 の各バージョンでベンチマーク実行します。

※標準出力をリダイレクトでMarkdownファイルに保存

php -f compare.php > compare.md

実行結果

▼PHP 8.2

▼PHP 8.3

▼PHP 8.4

バージョンの影響は殆どなく、大体似通った結果になりました。

あ、実行環境の記載を忘れてました。

  • PHP Version: 8.2.28 / 8.3.20 / 8.4.6
  • Laravel Framework Version: v12.9.2 / v12.10.0
  • OS: Ubuntu24.04.2 LTS on WSL2 (Windows11 26100.3775)
  • Machine: AMD Ryzen 7 7730U(2.00GHz) / RAM 16GB / 64bit

まとめ

Arr::dot() の爆速化が確認できました。

配列が大きい程、効果が大きいようです。

たしかに、array_merge() 使ってるけどいいのかな?と以前から思ってはいました。

Arr::dot() はドット記法での配列アクセスを実現させるための、Laravelの主要機能の一つです。

恩恵を受けている代表としては config()、view()、route() 、Validator あたりでしょうか。

例えばこんな感じ。

config('app.name')
view('post.create')
route('post.create')
$request->validate([
    'title' => 'required|unique:posts|max:255',
    'author.name' => 'required',
    'author.description' => 'required',
]);

Laravelを使わない筆者のプロジェクトでも Arr::dot() の実装を流用しているので、今回の最適化を反映させようと思います。

以上です。

コメント

タイトルとURLをコピーしました