LINQ для PHP: скорость имеет значение

a88804a0d87bfaf21b6817ade48e772b.pngЕсли вы не знаете, что такое 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

© Habrahabr.ru