LINQ for PHP 对比:YaLinqo, Ginq, Pinq





5.00/5 (3投票s)
对比了功能齐全的 LINQ for PHP 移植库 (YaLinqo, Ginq, Pinq),主要侧重于性能
- GitHub 项目:YaLinqoPerf — 性能对比
- GitHub 项目:YaLinqo — LINQ 库
- GitHub 项目:Ginq — LINQ 库
- GitHub 项目:Pinq — LINQ 库
- 下载 YaLinqoPerf 源代码 - 3 MB
引言
本文旨在对比 .NET 的 LINQ 移植到 PHP 的库(主要从性能方面)。在 .NET 中,LINQ 用于对各种集合(包括数据库)进行类 SQL 查询。在 PHP 中,它通常用于转换数组,类似于内置的 array_filter
和 array_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 查询是执行代码的重要组成部分,您可能更愿意继续使用老式的 for
和 foreach
。然而,我认为脚本语言不适合高负载项目,而且大部分繁重逻辑通常在数据库中完成,所以在大多数情况下,提高的可读性和可维护性值得一些性能损失。
有趣的是,这三个库的大小差异很大: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_map
和 array_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 PHP、Phinq、PHPLinq 和 Plinq。但是,我无法推荐使用任何一个。它们不完整、未经测试、文档不全,最重要的是,它们不是 LINQ — 它们都不支持惰性求值。在有更新的库的情况下,详细讨论它们将是浪费时间。
其中唯一值得一提的库是 PHPLinq。它支持查询数据库,实际上是很多数据库。但是,您应该考虑到该库几乎未经测试,函数调用顺序是固定的(它更像是生成 SQL 的 DAL),single
和 first
被认为是相同的等。我永远不会在生产环境中使用这样的代码,但您可以自己决定。
许可证
- YaLinqoPerf — WTFPL* 许可证
- YaLinqo — Simplified BSD 许可证
- Ginq — MIT 许可证
- Pinq — MIT 许可证 + BSD 3-clause 许可证(依赖项)
历史
- 2015-05-30:第一个版本