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

от автора

Если вы не знаете, что такое LINQ, и зачем он сдался на PHP, смотрите предыдущую статью по YaLinqo.

С остальными продолжаем. Сразу предупреждаю: если вы считаете, что итераторы — это ненужная штука, которую зачем-то притащили в PHP, что производительность из-за всех этих новомодных штучек с анонимными функциями зверски проседает, что нужно вымерять каждую микросекунду, что ничего лучше старого-доброго for не придумано — то проходите мимо. Библиотека и статья не для вас.

С остальными продолжаем. LINQ — это замечательно, но насколько проседает производительность от его использования? Если сравнивать с голыми циклами, то скорость меньше раз в 3-5. Если сравнивать с функциями для массивов, которым передаются анонимные функции, то раза в 2-4. Так как предполагается, что с помощью библиотеки обрабатываются небольшие массивы данных, а сложная обработка данных находится за пределами скрипта (в базе данных, в стороннем веб-сервисе), то на деле в масштабах всего скрипта потери небольшие. Главное — читаемость.

Так как со времени создания моей библиотеки YaLinqo на свет появилось ещё два конкурента, которые действительно являются LINQ (то есть поддерживают ленивые вычисления и прочие базовые возможности), то возникают позывы библиотеки сравнить. Самое простое и логичное — сравнить функциональность и производительность. По крайней мере это не будет избиением младенцев, как в прошлом сравнении.

(А также появление конкурентов наконец-то мотивировало меня выложить документацию YaLinqo онлайн.)

Дисклеймер: это тесты «на коленке». Они не дают оценить все потери в производительности. В частности, я совершенно не рассматриваю потребление памяти. Отчасти потому что я не знаю, как это нормально сделать. Если что, pull requests are welcome, что называется.

Конкуренты

YaLinqoYet 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 классов. Документация средней паршивости: в лучшем случае указаны сигнатуры. Главная особенность — итераторы, что позволяет использовать библиотеку и построением запросов в виде цепочки методов, и с помощью вложенных итераторов.

PinqPHP 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *