Если вы не знаете, что такое LINQ, и зачем он сдался на PHP, смотрите предыдущую статью по YaLinqo.
С остальными продолжаем. Сразу предупреждаю: если вы считаете, что итераторы — это ненужная штука, которую зачем-то притащили в PHP, что производительность из-за всех этих новомодных штучек с анонимными функциями зверски проседает, что нужно вымерять каждую микросекунду, что ничего лучше старого-доброго for не придумано — то проходите мимо. Библиотека и статья не для вас.
С остальными продолжаем. LINQ — это замечательно, но насколько проседает производительность от его использования? Если сравнивать с голыми циклами, то скорость меньше раз в 3-5. Если сравнивать с функциями для массивов, которым передаются анонимные функции, то раза в 2-4. Так как предполагается, что с помощью библиотеки обрабатываются небольшие массивы данных, а сложная обработка данных находится за пределами скрипта (в базе данных, в стороннем веб-сервисе), то на деле в масштабах всего скрипта потери небольшие. Главное — читаемость.
Так как со времени создания моей библиотеки YaLinqo на свет появилось ещё два конкурента, которые действительно являются LINQ (то есть поддерживают ленивые вычисления и прочие базовые возможности), то возникают позывы библиотеки сравнить. Самое простое и логичное — сравнить функциональность и производительность. По крайней мере это не будет избиением младенцев, как в прошлом сравнении.
(А также появление конкурентов наконец-то мотивировало меня выложить документацию YaLinqo онлайн.)
Дисклеймер: это тесты «на коленке». Они не дают оценить все потери в производительности. В частности, я совершенно не рассматриваю потребление памяти. Отчасти потому что я не знаю, как это нормально сделать. Если что, pull requests are welcome, что называется.
YaLinqo — Yet Another LINQ to Objects for PHP. Поддерживает запросы только к объектам: массивам и итераторам. Имеет две версии: для PHP 5.3+ (без yield) и для PHP 5.5+ (с yield). Последняя версия полагается исключительно на yield и массивы для всех операций. В дополнение к анонимным функциям поддерживает «строковые лямбды». Самая минималистичная из представленных библиотек: содержит всего лишь 4 класса. Из особенностей — весьма массивная документация, адаптированная из MSDN.
Ginq — ‘LINQ to Object’ inspired DSL for PHP . Аналогично, поддерживает запросы только к объектам. Основана на итераторах SPL, поэтому в требованиях PHP 5.3+. В дополнение к анонимным функциям поддерживает «property access» из Symfony. Средняя по масштабности библиотека: портированы коллекции, компареры, пары ключ-значение и прочее добро из .NET; итого 70 классов. Документация средней паршивости: в лучшем случае указаны сигнатуры. Главная особенность — итераторы, что позволяет использовать библиотеку и построением запросов в виде цепочки методов, и с помощью вложенных итераторов.
Pinq — PHP Integrated Query, a real LINQ library for PHP. Единственная библиотека, которая позволяет работать и с объектами, и с базами данных (ну… теоретически позволяет). Поддерживает только анонимные функции, но умеет парсить код с помощью PHP-Parser. Документация не самая детальная (если вообще есть), но зато имеет симпатичный сайтик. Самая массивная библиотека из представленных: больше 500 классов, не считая 150 классов тестов (если честно, в код я даже не лез, потому что страшно).
У всех представленных библиотек с тестами и прочими признаками качества всё в порядке. Лицензии пермиссивные: BSD, MIT. Все поддерживают Composer и представлены на Packagist.
Тесты
Здесь и далее в функцию benchmark_linq_groups
передаётся массив функций: для голого PHP, YaLinqo, Ginq и Pinq, соответственно.
Тесты гоняются на PHP 5.5.14, Windows 7 SP1. Так как тесты «на коленке», то не привожу спеки железа — задача оценить потери на глаз, а не измерить всё до миллиметра. Если хотите точных тестов, то исходный код доступен на гитхабе, можете улучшать, пулл-реквесты принимаются.
Начнём с плохого — чистого оверхеда.
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; }, ]);
Генерирующая функция range
в Pinq отсутствует, документация говорит пользоваться стандартной функцией. Что, собственно, мы и делаем.
И результаты:
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%)
Итераторы нещадно съедают скорость.
Но гораздо сильнее бросается в глаза страшное проседание по скорости у последней библиотеки — в 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 проигрывает только в два раза относительно решения в лоб на цикле. У остальных библиотек результаты похуже, но жить можно.
Теперь займёмся подсчётом в тестовых данных: посчитаем заказы с более, чем пятью пунктами заказа; посчитаем заказы, у которых более двух пунктов с количеством более пяти.
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(); }, ]);
Заметно три нюанса. Во-первых, функциональный стиль на стандартных функциях для массивов превращает код в забавную нечитаемую лесенку. Во-вторых, строковыми лямбдами воспользоваться не удаётся, потому что экранировать код внутри экранированного кода — это вынос мозга. В-третьих, Pinq не предоставляет функции count
, принимающей предикат, поэтому приходится строить цепочку методов. Как позже выяснится, это далеко не единственное ограничение Pinq: в ней очень мало методов и они очень сильно ограничены.
Смотрим результаты:
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, который предполагает создание по пути «анонимных типов» с промежуточными результатами вычислений.
Результаты, если сравнивать с предыдущими тестами, достаточно ровные:
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 вырывается вперёд, хоть и незначительно. Спойлер: это случилось в первый и последний раз.
Во-вторых, доступ к свойствам в Ginq ужасающе просаживает производительность, то есть в реальном коде этой фичей уже не воспользуешься. Синтаксис не стоит потери скорости в 50 раз.
Переходим к весёлому — к джойнам, ака соединению двух коллекций по ключу.
benchmark_linq_groups("Joining arrays", 100, 'consume', [ function () use ($DATA) { $usersByIds = [ ]; foreach ($DATA->users as $user) $usersByIds[$user['id']][] = $user; $pairs = [ ]; foreach ($DATA->orders as $order) { $id = $order['customerId']; if (isset($usersByIds[$id])) { foreach ($usersByIds[$id] as $user) { $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, где одна по сути функция разделена на несколько вызовов. Пожалуй, так более читаемо, но для привыкших к цепочкам методов в 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 действительно убивает скорость в шесть тысяч раз. Сначала я думал, что скрипт повис, но в конце концов он завершился, и выдал это невообразимое число. Я не нашёл, где в исходниках Pinq код для этого набора функций, но у меня ощущение, что там for-for-if без массивов-словарей. Вот вам и ООП.
Рассмотрим ещё один простой тест — аггрегацию (или аккумуляцию, или свёртку — как угодно):
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%)
Pinq и строковые свойства в Ginq показали страшненькие результаты, YaLinqo опечалил, встроенные функции опечалили не меньше. For рулит.
Ну и на десерт, пример из ReadMe YaLinqo — запрос со всеми функциями вместе взятыми:
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 ); }); }, ]);
Код на голом PHP написан общими усилиями здесь на Хабре.
Результаты:
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. Остальные показали более-менее ожидаемые результаты.
Подробнее о библиотеках
Так как Pinq — единственная из представленных библиотек, которая умеет формировать запросы SQL, распарсивая PHP, то статья будет неполной, если не рассмотреть эту возможность. К сожалению, как выяснилось, единственный провайдер — для MySQL, при этом он в виде «демонстрации». По сути, эта фича заявлена и может быть реализована на базе Pinq, но на деле воспользоваться ей невозможно.
Выводы
Если нужно быстренько отфильтровать сотню-другую результатов, полученных от веб-сервиса, то библиотеки LINQ вполне способны удовлетворить потребность.
Среди библиотек безоговорочный победитель по производительности — YaLinqo. Если нужно отфильтровать объекты с помощью запросов, то это самый логичный выбор.
Ginq может понравиться тем, кто предпочитает пользоваться не цепочками методов, а вложенными итераторами. Не знаю, есть ли такие ценители итераторов SPL.
Pinq на поверку оказался монструозной библиотекой, в которой некоторые возможности реализованы отвратительно, несмотря на множество слоёв абстракции. У этой библиотеки есть потенциал за счёт поддержки запросов к БД, но на данный момент он остаётся нереализованным.
Если нужны запросы к БД, то до сих пор остаётся единственный вариант — PHPLinq. Но использовать библиотеку весьма сомнительного качества нет смысла, потому что есть нормальные ORM библиотеки.
Ссылки
- YaLinqo — библиотека YaLinqo
- YaLinqo Docs — документация к библиотеке YaLinqo
- YaLinqo Perf — тесты на производительность YaLinqo, Ginq, Pinq
- Ginq — библиотека Ginq
- Pinq — библиотека Pinq
ссылка на оригинал статьи http://habrahabr.ru/post/259155/
Добавить комментарий