65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ for PHP 对比:YaLinqo, Ginq, Pinq

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2015年6月2日

CC (ASA 3U)

9分钟阅读

viewsIcon

40922

downloadIcon

132

对比了功能齐全的 LINQ for PHP 移植库 (YaLinqo, Ginq, Pinq),主要侧重于性能

引言

本文旨在对比 .NET 的 LINQ 移植到 PHP 的库(主要从性能方面)。在 .NET 中,LINQ 用于对各种集合(包括数据库)进行类 SQL 查询。在 PHP 中,它通常用于转换数组,类似于内置的 array_filterarray_map 函数,但形式更易读,功能也更强大。由于 PHP 的限制和当前库的状态,LINQ 移植库最适合对从 Web 服务返回的相对较小的数据集进行转换,例如。

这不是一篇入门文章,我不会包含 LINQ 的使用教程。我可能会写另一篇文章为初学者提供更多细节。但是,一些示例应该是自明的,所以即使您之前没有使用过 LINQ,您也可以对比代码。

您应该了解如何使用闭包。了解 LINQ 是一个巨大的优势。

背景

在开发另一个 .NET LINQ to PHP 移植库之前,我深入研究了所有可用的库。曾有很多:LINQ for PHP, Phinq, PHPLinq 和 Plinq。不幸的是,它们都不支持惰性求值;其中大部分没有足够(甚至没有)的测试;文档要么缺失要么不完整等。总的来说,它们明显不适合生产环境。

这就是 YaLinqo 诞生的原因。当时,它是唯一一个真正实现了 LINQ to objects 的 LINQ 移植库。它拥有 100% 的测试覆盖率,非常详细的 PHPDoc,支持“字符串 lambda”并且在转换过程中不丢失键。第一个版本是用 PHP 5.3 实现的,后来更新以利用 PHP 5.5 的 yield

自那时以来,出现了两个与 YaLinqo 竞争的库。第一个是 Ginq。与 YaLinqo 不同,它依赖于手动实现的迭代器。在某种程度上,它比第一个版本的 YaLinqo 更接近“PHP 风格”的实现,后者依赖于受 LINQ.js 启发的“hackish”迭代器。它不支持“字符串 lambda”,而是支持 Symfony 的“属性访问”,这在排序、分组和连接时非常方便。许多方法都有来自函数式编程的别名,例如“map”除了“select”。文档不详细。

另一个库是 Pinq。它是(潜在地)最强大的库,支持对象和数据库。它支持使用 PHP-Parser 解析 PHP 代码,并可以生成 SQL。不幸的是,在撰写本文时,唯一的查询提供者是 MySQL,并且其状态为“演示”。我怀疑在它准备好投入生产并开始支持多种 DBMS 之前还有很多工作要做。另一个缺点是,令人惊讶的是,它包含的功能更少,而且功能也不如其他库。

所有三个库都拥有宽松的开源许可证,良好的测试覆盖率,文档,支持大量函数,可在 Packagist 上获取,并且总体上可以用于任何不需要大量优化的项目。如果您计算每一微秒,您应该考虑到这些库会带来相当大的开销,因此如果您在高负载项目中使用它们,并且 LINQ 查询是执行代码的重要组成部分,您可能更愿意继续使用老式的 forforeach。然而,我认为脚本语言不适合高负载项目,而且大部分繁重逻辑通常在数据库中完成,所以在大多数情况下,提高的可读性和可维护性值得一些性能损失。

有趣的是,这三个库的大小差异很大:YaLinqo 包含 4 个类且没有依赖项,Ginq 包含 70 多个类且依赖于 Symfony 的 Property Access 模块,Pinq 包含 500 多个类且依赖于 PHP-Parser。区别在于它们的架构。YaLinqo 只使用 PHP 数组和回调。Pinq 为每种转换、集合、比较器等包含迭代器类。Ginq 包含更多受 .NET 中 LINQ 启发的类和接口,并包含支持数据库所需的所有底层组件:存储库、解析等。(我没有彻底研究 Pinq 的源代码。)

关于测试

我在性能测试方面经验很少,所以测试是快速而粗糙的,没有太多思考来获得精确的结果。内存使用率完全没有考虑。然而,性能差异如此之大,以至于我认为精度并不重要。如果您发现代码中的错误或可以改进测试,该项目可在 GitHub 上找到,欢迎提交 pull request。

在所有以下测试中,都调用了 benchmark_linq_groups 函数,该函数接受一个函数数组,用于 PHP、YaLinqo、Ginq 和 Pinq 的实现。此函数使用 foreach 消耗生成的集合,并确保所有测试返回的结果相同。

测试是在 PHP 5.5.14、Windows 7 SP1 64 位上进行的。

测试

让我们从纯开销开始

benchmark_linq_groups("Iterating over $ITER_MAX ints", 100, null,
    [
        "for" => function () use ($ITER_MAX) {
            $j = null;
            for ($i = 0; $i < $ITER_MAX; $i++)
                $j = $i;
            return $j;
        },
        "array functions" => function () use ($ITER_MAX) {
            $j = null;
            foreach (range(0, $ITER_MAX - 1) as $i)
                $j = $i;
            return $j;
        },
    ],
    [
        function () use ($ITER_MAX) {
            $j = null;
            foreach (E::range(0, $ITER_MAX) as $i)
                $j = $i;
            return $j;
        },
    ],
    [
        function () use ($ITER_MAX) {
            $j = null;
            foreach (G::range(0, $ITER_MAX - 1) as $i)
                $j = $i;
            return $j;
        },
    ],
    [
        function () use ($ITER_MAX) {
            $j = null;
            foreach (P::from(range(0, $ITER_MAX - 1)) as $i)
                $j = $i;
            return $j;
        },
    ]);

Pinq 中没有 range 生成器函数,因此我按照其文档的建议,使用内置函数。

这是结果:

Iterating over 1000 ints
------------------------
  PHP     [for]               0.00006 sec   x1.0 (100%)
  PHP     [array functions]   0.00011 sec   x1.8 (+83%)
  YaLinqo                     0.00041 sec   x6.8 (+583%)
  Ginq                        0.00075 sec   x12.5 (+1150%)
  Pinq                        0.00169 sec   x28.2 (+2717%)

迭代器浪费了很多时间。Pinq 的表现最令人惊讶 — 比 for 慢 30 倍。然而,这远非最令人惊讶的结果,您将看到。

让我们生成一个数组而不是仅仅迭代

benchmark_linq_groups("Generating array of $ITER_MAX integers", 100, 'consume',
    [
        "for" =>
            function () use ($ITER_MAX) {
                $a = [ ];
                for ($i = 0; $i < $ITER_MAX; $i++)
                    $a[] = $i;
                return $a;
            },
        "array functions" =>
            function () use ($ITER_MAX) {
                return range(0, $ITER_MAX - 1);
            },
    ],
    [
        function () use ($ITER_MAX) {
            return E::range(0, $ITER_MAX)->toArray();
        },
    ],
    [
        function () use ($ITER_MAX) {
            return G::range(0, $ITER_MAX - 1)->toArray();
        },
    ],
    [
        function () use ($ITER_MAX) {
            return P::from(range(0, $ITER_MAX - 1))->asArray();
        },
    ]);

以及结果

Generating array of 1000 integers
---------------------------------
  PHP     [for]               0.00025 sec   x1.3 (+32%)
  PHP     [array functions]   0.00019 sec   x1.0 (100%)
  YaLinqo                     0.00060 sec   x3.2 (+216%)
  Ginq                        0.00107 sec   x5.6 (+463%)
  Pinq                        0.00183 sec   x9.6 (+863%)

YaLinqo 现在只比使用 for 的解决方案慢两倍。其他库表现更差,但尚可接受。

让我们统计测试数据中的项:订单数量大于 5 个订单项;订单数量大于 2 个订单项且数量大于 5 个。

benchmark_linq_groups("Counting values in arrays", 100, null,
    [
        "for" => function () use ($DATA) {
            $numberOrders = 0;
            foreach ($DATA->orders as $order) {
                if (count($order['items']) > 5)
                    $numberOrders++;
            }
            return $numberOrders;
        },
        "array functions" => function () use ($DATA) {
            return count(
                array_filter(
                    $DATA->orders,
                    function ($order) { return count($order['items']) > 5; }
                )
            );
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->orders)
                ->count(function ($order) { return count($order['items']) > 5; });
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->orders)
                ->count('$o ==> count($o["items"]) > 5');
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->orders)
                ->count(function ($order) { return count($order['items']) > 5; });
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->orders)
                ->where(function ($order) { return count($order['items']) > 5; })
                ->count();
        },
    ]);

benchmark_linq_groups("Counting values in arrays deep", 100, null,
    [
        "for" => function () use ($DATA) {
            $numberOrders = 0;
            foreach ($DATA->orders as $order) {
                $numberItems = 0;
                foreach ($order['items'] as $item) {
                    if ($item['quantity'] > 5)
                        $numberItems++;
                }
                if ($numberItems > 2)
                    $numberOrders++;
            }
            return $numberOrders;
        },
        "array functions" => function () use ($DATA) {
            return count(
                array_filter(
                    $DATA->orders,
                    function ($order) {
                        return count(
                            array_filter(
                                $order['items'],
                                function ($item) { return $item['quantity'] > 5; }
                            )
                        ) > 2;
                    })
            );
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->orders)
                ->count(function ($order) {
                    return E::from($order['items'])
                        ->count(function ($item) { return $item['quantity'] > 5; }) > 2;
                });
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->orders)
                ->count(function ($order) {
                    return G::from($order['items'])
                        ->count(function ($item) { return $item['quantity'] > 5; }) > 2;
                });
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->orders)
                ->where(function ($order) {
                    return P::from($order['items'])
                        ->where(function ($item) { return $item['quantity'] > 5; })
                        ->count() > 2;
                })
                ->count();
        },
    ]);

要点:首先,使用标准数组函数的函数式风格会使代码变成滑稽的、难以阅读的阶梯。第二,“字符串 lambda”在这里没有帮助,因为在转义的代码中转义代码是不可理解的。第三,Pinq 没有提供接受谓词的 count 函数重载,因此需要方法链。结果

Counting values in arrays
-------------------------
  PHP     [for]               0.00023 sec   x1.0 (100%)
  PHP     [array functions]   0.00052 sec   x2.3 (+126%)
  YaLinqo                     0.00056 sec   x2.4 (+143%)
  YaLinqo [string lambda]     0.00059 sec   x2.6 (+157%)
  Ginq                        0.00129 sec   x5.6 (+461%)
  Pinq                        0.00382 sec   x16.6 (+1561%)

Counting values in arrays deep
------------------------------
  PHP     [for]               0.00064 sec   x1.0 (100%)
  PHP     [array functions]   0.00323 sec   x5.0 (+405%)
  YaLinqo                     0.00798 sec   x12.5 (+1147%)
  Ginq                        0.01416 sec   x22.1 (+2113%)
  Pinq                        0.04928 sec   x77.0 (+7600%)

结果或多或少都在预期之中,除了可怕的 Pinq 结果。我看了代码——它似乎生成了一个完整的集合,然后在其上调用内置的 count...

让我们过滤数组。条件与上次相同,但不是计数,而是生成集合。

benchmark_linq_groups("Filtering values in arrays", 100, 'consume',
    [
        "for" => function () use ($DATA) {
            $filteredOrders = [ ];
            foreach ($DATA->orders as $order) {
                if (count($order['items']) > 5)
                    $filteredOrders[] = $order;
            }
            return $filteredOrders;
        },
        "array functions" => function () use ($DATA) {
            return array_filter(
                $DATA->orders,
                function ($order) { return count($order['items']) > 5; }
            );
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->orders)
                ->where(function ($order) { return count($order['items']) > 5; });
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->orders)
                ->where('$order ==> count($order["items"]) > 5');
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->orders)
                ->where(function ($order) { return count($order['items']) > 5; });
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->orders)
                ->where(function ($order) { return count($order['items']) > 5; });
        },
    ]);

benchmark_linq_groups("Filtering values in arrays deep", 100,
    function ($e) { consume($e, [ 'items' => null ]); },
    [
        "for" => function () use ($DATA) {
            $filteredOrders = [ ];
            foreach ($DATA->orders as $order) {
                $filteredItems = [ ];
                foreach ($order['items'] as $item) {
                    if ($item['quantity'] > 5)
                        $filteredItems[] = $item;
                }
                if (count($filteredItems) > 0) {
                    $order['items'] = $filteredItems;
                    $filteredOrders[] = [
                        'id' => $order['id'],
                        'items' => $filteredItems,
                    ];
                }
            }
            return $filteredOrders;
        },
        "array functions" => function () use ($DATA) {
            return array_filter(
                array_map(
                    function ($order) {
                        return [
                            'id' => $order['id'],
                            'items' => array_filter(
                                $order['items'],
                                function ($item) { return $item['quantity'] > 5; }
                            )
                        ];
                    },
                    $DATA->orders
                ),
                function ($order) {
                    return count($order['items']) > 0;
                }
            );
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->orders)
                ->select(function ($order) {
                    return [
                        'id' => $order['id'],
                        'items' => E::from($order['items'])
                            ->where(function ($item) { return $item['quantity'] > 5; })
                            ->toArray()
                    ];
                })
                ->where(function ($order) {
                    return count($order['items']) > 0;
                });
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->orders)
                ->select(function ($order) {
                    return [
                        'id' => $order['id'],
                        'items' => E::from($order['items'])->where('$v["quantity"] > 5')->toArray()
                    ];
                })
                ->where('count($v["items"]) > 0');
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->orders)
                ->select(function ($order) {
                    return [
                        'id' => $order['id'],
                        'items' => G::from($order['items'])
                            ->where(function ($item) { return $item['quantity'] > 5; })
                            ->toArray()
                    ];
                })
                ->where(function ($order) {
                    return count($order['items']) > 0;
                });
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->orders)
                ->select(function ($order) {
                    return [
                        'id' => $order['id'],
                        'items' => P::from($order['items'])
                            ->where(function ($item) { return $item['quantity'] > 5; })
                            ->asArray()
                    ];
                })
                ->where(function ($order) {
                    return count($order['items']) > 0;
                });
        },
    ]);

使用标准数组函数的代码变得非常难以理解,主要是由于 array_maparray_filter 的参数顺序不一致。

使用 LINQ 的代码故意不是最优的:即使对象稍后会被丢弃,也会生成对象。在 LINQ 中,使用“匿名对象”在转换之间传递数据是一种传统。

与之前的结果相比,这些结果异常地均匀

Filtering values in arrays
--------------------------
  PHP     [for]               0.00049 sec   x1.0 (100%)
  PHP     [array functions]   0.00072 sec   x1.5 (+47%)
  YaLinqo                     0.00094 sec   x1.9 (+92%)
  YaLinqo [string lambda]     0.00094 sec   x1.9 (+92%)
  Ginq                        0.00295 sec   x6.0 (+502%)
  Pinq                        0.00328 sec   x6.7 (+569%)

Filtering values in arrays deep
-------------------------------
  PHP     [for]               0.00514 sec   x1.0 (100%)
  PHP     [array functions]   0.00739 sec   x1.4 (+44%)
  YaLinqo                     0.01556 sec   x3.0 (+203%)
  YaLinqo [string lambda]     0.01750 sec   x3.4 (+240%)
  Ginq                        0.03101 sec   x6.0 (+503%)
  Pinq                        0.05435 sec   x10.6 (+957%)

让我们进行排序

benchmark_linq_groups("Sorting arrays", 100, 'consume',
    [
        function () use ($DATA) {
            $orderedUsers = $DATA->users;
            usort(
                $orderedUsers,
                function ($a, $b) {
                    $diff = $a['rating'] - $b['rating'];
                    if ($diff !== 0)
                        return -$diff;
                    $diff = strcmp($a['name'], $b['name']);
                    if ($diff !== 0)
                        return $diff;
                    $diff = $a['id'] - $b['id'];
                    return $diff;
                });
            return $orderedUsers;
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->users)
                ->orderByDescending(function ($u) { return $u['rating']; })
                ->thenBy(function ($u) { return $u['name']; })
                ->thenBy(function ($u) { return $u['id']; });
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->users)
                ->orderByDescending('$v["rating"]')->thenBy('$v["name"]')->thenBy('$v["id"]');
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->users)
                ->orderByDesc(function ($u) { return $u['rating']; })
                ->thenBy(function ($u) { return $u['name']; })
                ->thenBy(function ($u) { return $u['id']; });
        },
        "property path" => function () use ($DATA) {
            return G::from($DATA->users)
                ->orderByDesc('[rating]')->thenBy('[name]')->thenBy('[id]');
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->users)
                ->orderByDescending(function ($u) { return $u['rating']; })
                ->thenByAscending(function ($u) { return $u['name']; })
                ->thenByAscending(function ($u) { return $u['id']; });
        },
    ]);

usort 的回调代码有点吓人,但经过一些练习,编写比较器代码非常容易。使用 LINQ 的代码非常简洁,尤其是在 Ginq 中,“属性访问”使代码更加美观。

结果出乎意料

Sorting arrays
--------------
  PHP                         0.00037 sec   x1.0 (100%)
  YaLinqo                     0.00161 sec   x4.4 (+335%)
  YaLinqo [string lambda]     0.00163 sec   x4.4 (+341%)
  Ginq                        0.00402 sec   x10.9 (+986%)
  Ginq    [property path]     0.01998 sec   x54.0 (+5300%)
  Pinq                        0.00132 sec   x3.6 (+257%)

首先,Pinq 在 LINQ 库中首次(剧透:也是最后一次)最快。

其次,Ginq 的属性访问速度非常慢。我认为它无法使用,因为它们不值得 50 倍的时间增加。

我们进入有趣的部分 — 基于两个数组中相等的键将它们连接起来。

benchmark_linq_groups("Joining arrays", 100, 'consume',
    [
        function () use ($DATA) {
            $ordersByCustomerId = [ ];
            foreach ($DATA->orders as $order)
                $ordersByCustomerId[$order['customerId']][] = $order;
            $pairs = [ ];
            foreach ($DATA->users as $user) {
                $userId = $user['id'];
                if (isset($ordersByCustomerId[$userId])) {
                    foreach ($ordersByCustomerId[$userId] as $order) {
                        $pairs[] = [
                            'order' => $order,
                            'user' => $user,
                        ];
                    }
                }
            }
            return $pairs;
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->orders)
                ->join($DATA->users,
                    function ($o) { return $o['customerId']; },
                    function ($u) { return $u['id']; },
                    function ($o, $u) {
                        return [
                            'order' => $o,
                            'user' => $u,
                        ];
                    });
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->orders)
                ->join($DATA->users,
                    '$o ==> $o["customerId"]', '$u ==> $u["id"]',
                    '($o, $u) ==> [
                        "order" => $o,
                        "user" => $u,
                    ]');
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->orders)
                ->join($DATA->users,
                    function ($o) { return $o['customerId']; },
                    function ($u) { return $u['id']; },
                    function ($o, $u) {
                        return [
                            'order' => $o,
                            'user' => $u,
                        ];
                    });
        },
        "property path" => function () use ($DATA) {
            return G::from($DATA->orders)
                ->join($DATA->users,
                    '[customerId]', '[id]',
                    function ($o, $u) {
                        return [
                            'order' => $o,
                            'user' => $u,
                        ];
                    });
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->orders)
                ->join($DATA->users)
                ->onEquality(
                    function ($o) { return $o['customerId']; },
                    function ($u) { return $u['id']; }
                )
                ->to(function ($o, $u) {
                    return [
                        'order' => $o,
                        'user' => $u,
                    ];
                });
        },
    ]);

Pinq 的代码与其他代码不同。它将单个方法调用转换为链。这提高了可读性,但对于习惯了 .NET 中 LINQ 方法链的人来说,可能看起来不寻常。

以及结果

Joining arrays
--------------
  PHP                         0.00021 sec   x1.0 (100%)
  YaLinqo                     0.00065 sec   x3.1 (+210%)
  YaLinqo [string lambda]     0.00070 sec   x3.3 (+233%)
  Ginq                        0.00103 sec   x4.9 (+390%)
  Ginq    [property path]     0.00200 sec   x9.5 (+852%)
  Pinq                        1.24155 sec   x5,911.8 (+591084%)

哇。简直是哇。不,这不是玩笑。我以为脚本卡住了,但最终它返回了这个惊人的结果。Pinq 比原始 PHP 慢 5,912 倍。我找不到 Plinq 代码中具体是哪里发生这种情况,但看起来它基本上是 for-for-if 而没有查找。我完全没料到一位实现了 500 个类的开发人员会这样做。

好的,让我们看一个更简单的测试 — 聚合(或累积,或折叠)。

benchmark_linq_groups("Aggregating arrays", 100, null,
    [
        "for" => function () use ($DATA) {
            $sum = 0;
            foreach ($DATA->products as $p)
                $sum += $p['quantity'];
            $avg = 0;
            foreach ($DATA->products as $p)
                $avg += $p['quantity'];
            $avg /= count($DATA->products);
            $min = PHP_INT_MAX;
            foreach ($DATA->products as $p)
                $min = min($min, $p['quantity']);
            $max = -PHP_INT_MAX;
            foreach ($DATA->products as $p)
                $max = max($max, $p['quantity']);
            return "$sum-$avg-$min-$max";
        },
        "array functions" => function () use ($DATA) {
            $sum = array_sum(array_map(function ($p) { return $p['quantity']; }, $DATA->products));
            $avg = array_sum(array_map(function ($p) { return $p['quantity']; }, $DATA->products)) / count($DATA->products);
            $min = min(array_map(function ($p) { return $p['quantity']; }, $DATA->products));
            $max = max(array_map(function ($p) { return $p['quantity']; }, $DATA->products));
            return "$sum-$avg-$min-$max";
        },
    ],
    [
        function () use ($DATA) {
            $sum = E::from($DATA->products)->sum(function ($p) { return $p['quantity']; });
            $avg = E::from($DATA->products)->average(function ($p) { return $p['quantity']; });
            $min = E::from($DATA->products)->min(function ($p) { return $p['quantity']; });
            $max = E::from($DATA->products)->max(function ($p) { return $p['quantity']; });
            return "$sum-$avg-$min-$max";
        },
        "string lambda" => function () use ($DATA) {
            $sum = E::from($DATA->products)->sum('$v["quantity"]');
            $avg = E::from($DATA->products)->average('$v["quantity"]');
            $min = E::from($DATA->products)->min('$v["quantity"]');
            $max = E::from($DATA->products)->max('$v["quantity"]');
            return "$sum-$avg-$min-$max";
        },
    ],
    [
        function () use ($DATA) {
            $sum = G::from($DATA->products)->sum(function ($p) { return $p['quantity']; });
            $avg = G::from($DATA->products)->average(function ($p) { return $p['quantity']; });
            $min = G::from($DATA->products)->min(function ($p) { return $p['quantity']; });
            $max = G::from($DATA->products)->max(function ($p) { return $p['quantity']; });
            return "$sum-$avg-$min-$max";
        },
        "property path" => function () use ($DATA) {
            $sum = G::from($DATA->products)->sum('[quantity]');
            $avg = G::from($DATA->products)->average('[quantity]');
            $min = G::from($DATA->products)->min('[quantity]');
            $max = G::from($DATA->products)->max('[quantity]');
            return "$sum-$avg-$min-$max";
        },
    ],
    [
        function () use ($DATA) {
            $sum = P::from($DATA->products)->sum(function ($p) { return $p['quantity']; });
            $avg = P::from($DATA->products)->average(function ($p) { return $p['quantity']; });
            $min = P::from($DATA->products)->minimum(function ($p) { return $p['quantity']; });
            $max = P::from($DATA->products)->maximum(function ($p) { return $p['quantity']; });
            return "$sum-$avg-$min-$max";
        },
    ]);

benchmark_linq_groups("Aggregating arrays custom", 100, null,
    [
        function () use ($DATA) {
            $mult = 1;
            foreach ($DATA->products as $p)
                $mult *= $p['quantity'];
            return $mult;
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->products)->aggregate(function ($a, $p) { return $a * $p['quantity']; }, 1);
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->products)->aggregate('$a * $v["quantity"]', 1);
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->products)->aggregate(1, function ($a, $p) { return $a * $p['quantity']; });
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->products)
                ->select(function ($p) { return $p['quantity']; })
                ->aggregate(function ($a, $q) { return $a * $q; });
        },
    ]);

第一组函数没什么好解释的。

在第二组中,我计算乘法(是的,乘以产品数量意义不大,但谁在乎)。Pinq 没有接受种子参数的重载,它总是使用第一个元素(如果没有元素,它还会静默返回 null...),所以我又不得不使用方法链。

结果

Aggregating arrays
------------------
  PHP     [for]               0.00059 sec   x1.0 (100%)
  PHP     [array functions]   0.00193 sec   x3.3 (+227%)
  YaLinqo                     0.00475 sec   x8.1 (+705%)
  YaLinqo [string lambda]     0.00515 sec   x8.7 (+773%)
  Ginq                        0.00669 sec   x11.3 (+1034%)
  Ginq    [property path]     0.03955 sec   x67.0 (+6603%)
  Pinq                        0.03226 sec   x54.7 (+5368%)

Aggregating arrays custom
-------------------------
  PHP                         0.00007 sec   x1.0 (100%)
  YaLinqo                     0.00046 sec   x6.6 (+557%)
  YaLinqo [string lambda]     0.00057 sec   x8.1 (+714%)
  Ginq                        0.00046 sec   x6.6 (+557%)
  Pinq                        0.00610 sec   x87.1 (+8615%)

所有 LINQ 库的表现都很糟糕。Ginq 在属性访问模式下和 Pinq 的表现尤其糟糕。即使是内置函数也远非高效。For 获胜。

最后,最后一个测试,来自 YaLinqo 的 ReadMe 中的一个复杂查询,它使用了几个函数和子查询

benchmark_linq_groups("Process data from ReadMe example", 5,
    function ($e) { consume($e, [ 'products' => null ]); },
    [
        function () use ($DATA) {
            $productsSorted = [ ];
            foreach ($DATA->products as $product) {
                if ($product['quantity'] > 0) {
                    if (empty($productsSorted[$product['catId']]))
                        $productsSorted[$product['catId']] = [ ];
                    $productsSorted[$product['catId']][] = $product;
                }
            }
            foreach ($productsSorted as $catId => $products) {
                usort($productsSorted[$catId], function ($a, $b) {
                    $diff = $a['quantity'] - $b['quantity'];
                    if ($diff != 0)
                        return -$diff;
                    $diff = strcmp($a['name'], $b['name']);
                    return $diff;
                });
            }
            $result = [ ];
            $categoriesSorted = $DATA->categories;
            usort($categoriesSorted, function ($a, $b) {
                return strcmp($a['name'], $b['name']);
            });
            foreach ($categoriesSorted as $category) {
                $categoryId = $category['id'];
                $result[$category['id']] = [
                    'name' => $category['name'],
                    'products' => isset($productsSorted[$categoryId]) ? $productsSorted[$categoryId] : [ ],
                ];
            }
            return $result;
        },
    ],
    [
        function () use ($DATA) {
            return E::from($DATA->categories)
                ->orderBy(function ($cat) { return $cat['name']; })
                ->groupJoin(
                    from($DATA->products)
                        ->where(function ($prod) { return $prod['quantity'] > 0; })
                        ->orderByDescending(function ($prod) { return $prod['quantity']; })
                        ->thenBy(function ($prod) { return $prod['name']; }),
                    function ($cat) { return $cat['id']; },
                    function ($prod) { return $prod['catId']; },
                    function ($cat, $prods) {
                        return array(
                            'name' => $cat['name'],
                            'products' => $prods
                        );
                    }
                );
        },
        "string lambda" => function () use ($DATA) {
            return E::from($DATA->categories)
                ->orderBy('$cat ==> $cat["name"]')
                ->groupJoin(
                    from($DATA->products)
                        ->where('$prod ==> $prod["quantity"] > 0')
                        ->orderByDescending('$prod ==> $prod["quantity"]')
                        ->thenBy('$prod ==> $prod["name"]'),
                    '$cat ==> $cat["id"]', '$prod ==> $prod["catId"]',
                    '($cat, $prods) ==> [
                            "name" => $cat["name"],
                            "products" => $prods
                        ]');
        },
    ],
    [
        function () use ($DATA) {
            return G::from($DATA->categories)
                ->orderBy(function ($cat) { return $cat['name']; })
                ->groupJoin(
                    G::from($DATA->products)
                        ->where(function ($prod) { return $prod['quantity'] > 0; })
                        ->orderByDesc(function ($prod) { return $prod['quantity']; })
                        ->thenBy(function ($prod) { return $prod['name']; }),
                    function ($cat) { return $cat['id']; },
                    function ($prod) { return $prod['catId']; },
                    function ($cat, $prods) {
                        return array(
                            'name' => $cat['name'],
                            'products' => $prods
                        );
                    }
                );
        },
    ],
    [
        function () use ($DATA) {
            return P::from($DATA->categories)
                ->orderByAscending(function ($cat) { return $cat['name']; })
                ->groupJoin(
                    P::from($DATA->products)
                        ->where(function ($prod) { return $prod['quantity'] > 0; })
                        ->orderByDescending(function ($prod) { return $prod['quantity']; })
                        ->thenByAscending(function ($prod) { return $prod['name']; })
                )
                ->onEquality(
                    function ($cat) { return $cat['id']; },
                    function ($prod) { return $prod['catId']; }
                )
                ->to(function ($cat, $prods) {
                    return array(
                        'name' => $cat['name'],
                        'products' => $prods
                    );
                });
        },
    ]);

结果

Process data from ReadMe example
--------------------------------
  PHP                         0.00620 sec   x1.0 (100%)
  YaLinqo                     0.02840 sec   x4.6 (+358%)
  YaLinqo [string lambda]     0.02920 sec   x4.7 (+371%)
  Ginq                        0.07720 sec   x12.5 (+1145%)
  Pinq                        2.71616 sec   x438.1 (+43707%)

GroupJoin 摧毁了 Pinq 的性能。我猜原因与 join 测试中的原因相同。

所有结果

Iterating over 1000 ints
------------------------
  PHP     [for]               0.00006 sec   x1.0 (100%)
  PHP     [array functions]   0.00011 sec   x1.8 (+83%)
  YaLinqo                     0.00041 sec   x6.8 (+583%)
  Ginq                        0.00075 sec   x12.5 (+1150%)
  Pinq                        0.00169 sec   x28.2 (+2717%)

Generating array of 1000 integers
---------------------------------
  PHP     [for]               0.00025 sec   x1.3 (+32%)
  PHP     [array functions]   0.00019 sec   x1.0 (100%)
  YaLinqo                     0.00060 sec   x3.2 (+216%)
  Ginq                        0.00107 sec   x5.6 (+463%)
  Pinq                        0.00183 sec   x9.6 (+863%)

Generating lookup of 1000 floats, calculate sum
-----------------------------------------------
  PHP                         0.00124 sec   x1.0 (100%)
  YaLinqo                     0.00381 sec   x3.1 (+207%)
  YaLinqo [string lambda]     0.00403 sec   x3.3 (+225%)
  Ginq                        0.01390 sec   x11.2 (+1021%)
  Pinq                        * Not implemented

Counting values in arrays
-------------------------
  PHP     [for]               0.00023 sec   x1.0 (100%)
  PHP     [arrays functions]  0.00052 sec   x2.3 (+126%)
  YaLinqo                     0.00056 sec   x2.4 (+143%)
  YaLinqo [string lambda]     0.00059 sec   x2.6 (+157%)
  Ginq                        0.00129 sec   x5.6 (+461%)
  Pinq                        0.00382 sec   x16.6 (+1561%)

Counting values in arrays deep
------------------------------
  PHP     [for]               0.00064 sec   x1.0 (100%)
  PHP     [arrays functions]  0.00323 sec   x5.0 (+405%)
  YaLinqo                     0.00798 sec   x12.5 (+1147%)
  Ginq                        0.01416 sec   x22.1 (+2113%)
  Pinq                        0.04928 sec   x77.0 (+7600%)

Filtering values in arrays
--------------------------
  PHP     [for]               0.00049 sec   x1.0 (100%)
  PHP     [arrays functions]  0.00072 sec   x1.5 (+47%)
  YaLinqo                     0.00094 sec   x1.9 (+92%)
  YaLinqo [string lambda]     0.00094 sec   x1.9 (+92%)
  Ginq                        0.00295 sec   x6.0 (+502%)
  Pinq                        0.00328 sec   x6.7 (+569%)

Filtering values in arrays deep
-------------------------------
  PHP     [for]               0.00514 sec   x1.0 (100%)
  PHP     [arrays functions]  0.00739 sec   x1.4 (+44%)
  YaLinqo                     0.01556 sec   x3.0 (+203%)
  YaLinqo [string lambda]     0.01750 sec   x3.4 (+240%)
  Ginq                        0.03101 sec   x6.0 (+503%)
  Pinq                        0.05435 sec   x10.6 (+957%)

Sorting arrays
--------------
  PHP                         0.00037 sec   x1.0 (100%)
  YaLinqo                     0.00161 sec   x4.4 (+335%)
  YaLinqo [string lambda]     0.00163 sec   x4.4 (+341%)
  Ginq                        0.00402 sec   x10.9 (+986%)
  Ginq    [property path]     0.01998 sec   x54.0 (+5300%)
  Pinq                        0.00132 sec   x3.6 (+257%)

Joining arrays
--------------
  PHP                         0.00016 sec   x1.0 (100%)
  YaLinqo                     0.00065 sec   x4.1 (+306%)
  YaLinqo [string lambda]     0.00070 sec   x4.4 (+337%)
  Ginq                        0.00105 sec   x6.6 (+556%)
  Ginq    [property path]     0.00194 sec   x12.1 (+1112%)
  Pinq                        1.21249 sec   x7,577.5 (+757648%)

Aggregating arrays
------------------
  PHP     [for]               0.00059 sec   x1.0 (100%)
  PHP     [array functions]   0.00193 sec   x3.3 (+227%)
  YaLinqo                     0.00475 sec   x8.1 (+705%)
  YaLinqo [string lambda]     0.00515 sec   x8.7 (+773%)
  Ginq                        0.00669 sec   x11.3 (+1034%)
  Ginq    [property path]     0.03955 sec   x67.0 (+6603%)
  Pinq                        0.03226 sec   x54.7 (+5368%)

Aggregating arrays custom
-------------------------
  PHP                         0.00007 sec   x1.0 (100%)
  YaLinqo                     0.00046 sec   x6.6 (+557%)
  YaLinqo [string lambda]     0.00057 sec   x8.1 (+714%)
  Ginq                        0.00046 sec   x6.6 (+557%)
  Pinq                        0.00610 sec   x87.1 (+8615%)

Process data from ReadMe example
--------------------------------
  PHP                         0.00620 sec   x1.0 (100%)
  YaLinqo                     0.02840 sec   x4.6 (+358%)
  YaLinqo [string lambda]     0.02920 sec   x4.7 (+371%)
  Ginq                        0.07720 sec   x12.5 (+1145%)
  Pinq                        2.71616 sec   x438.1 (+43707%)

结论

如果您需要对相对较小的数据集执行查询,例如从 Web 服务返回的数据,您可以使用 YaLinqo 或 Ginq。

YaLinqo 性能更好,函数更多,文档更好。它是一个极简主义的库,依赖于现代 PHP 功能。它支持匿名函数和字符串 lambda(各种形式)。除了对迭代器的包装器,它不包含任何类,并依赖于经典的 PHP 数组,因此易于学习。

Ginq 使用了多个迭代器、集合和比较器类。因此,它更接近 .NET 的 LINQ。但这是有代价的。与 .NET 不同,PHP 中实现的自定义字典比原生数组慢得多。另一方面,公共迭代器类对于 .NET 开发者来说是陌生的,但使用 SPL 的 PHP 开发者习惯于看到它们。而且它们也有代价——使用 SPL 迭代器进行迭代比 yield 慢得多。总的来说,Ginq 比 YaLinqo 慢 1.5—3 倍。

Pinq 慢得离谱。没有任何架构可以证明因为一个简单的查询而将应用程序速度降低 6000 倍。该库有一个漂亮的网站,一个支持数据库的独特功能,一个复杂的架构,它已经是第 3 版了,所以我非常遗憾地得出结论,该库完全无法使用。我希望开发者能提高性能并实现至少一个功能齐全的查询提供者。当完成时,该库可能成为需要 LINQ to database 时的首选库。

另一个值得考虑的库是 Underscore.php。它不是 LINQ,也不是惰性的,但它遵循相同的函数式思想,如果您使用过函数式语言或其他语言中的各种 Underscore.* 库,它的方法可能会让您感到熟悉。

其他库

我用俄语写了一篇关于早期“LINQ”库的广泛文章:LINQ for PHPPhinqPHPLinqPlinq。但是,我无法推荐使用任何一个。它们不完整、未经测试、文档不全,最重要的是,它们不是 LINQ — 它们都不支持惰性求值。在有更新的库的情况下,详细讨论它们将是浪费时间。

其中唯一值得一提的库是 PHPLinq。它支持查询数据库,实际上是很多数据库。但是,您应该考虑到该库几乎未经测试,函数调用顺序是固定的(它更像是生成 SQL 的 DAL),singlefirst 被认为是相同的等。我永远不会在生产环境中使用这样的代码,但您可以自己决定。

许可证

  • YaLinqoPerf — WTFPL* 许可证
  • YaLinqo — Simplified BSD 许可证
  • Ginq — MIT 许可证
  • Pinq — MIT 许可证 + BSD 3-clause 许可证(依赖项)

历史

  • 2015-05-30:第一个版本
© . All rights reserved.