TM Feed Хабрахабр Мегамозг Geektimes Тостер Мой круг Фрилансим

Спецпроекты:

Разделы

Публикации Хабы Компании Пользователи Песочница

Q&A Вакансии

Спецпроекты

$27000 на хостинг Соберите свой стартап Обзор колонок Marshall

Привет, Хабрапользователь. Кажется, ты используешь AdBlock. Хабрахабр
развивается и существует за счет доходов от рекламы. Добавь нас в исключения.
открытое письмо как отключить

Александр Прохоров @Athari

Программист C#

98,5

карма

0,3

рейтинг

Профиль

16 Публикации

2k Комментарии

231 Избранное

15 Подписчики

1 июня 2015 в 14:14

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

Функциональное программирование*, Программирование*, Веб-разработка*, PHP*, Open source*

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

php,

linq,

yalinqo,

pinq,

ginq,

performance,

linq to objects,

functional programming,

похапэ,

запросы

↑
+12
↓

10,6k

Добавить в избранное 76

Александр Прохоров @Athari

карма 98,5

рейтинг 0,3

Программист C#

Реклама

Похожие публикации

+46

Сложно о простом или особенности Linq to objects 2,8k 65 10

+23

Linq To Entities vs. Linq To Objects на примере группировки 3,5k 30 23

+5

LINQ to Objects на примерах 19,9k 40 12

Реклама

Самое читаемое

Сейчас

Неделя

Месяц

Пишите код, который легко удалять, а не дополнять  7

Покопаемся в «режиссёрской версии» Wasteland 2: механики и интерфейсы  23

Удобное преобразование перечислений (enum) в строковые в С++  6

Как работает Google Поиск?  0

Как наши технари в армии служили  56

Обзор IntelliJ IDEA 16 Public Preview  56

LLVM: компилятор своими руками. Введение  9

10 тезисов инди-разработки, которые привели к успеху  8

Metronome IM и Jappix: многофункциональный Jabber, без сложностей в настройке  5

Открытые курсы Android StudyJams по всей России  6

Новый криптовымогатель «Locky»  67

Мурат Уртембаев — первый советский хакер  36

Мои правила хорошего интерфейса  24

Как наши технари в армии служили  56

6 впечатляющих веб-технологий 2015 года  37

Поняв Docker  194

Как выбрать фреймворк для frontend-разработки  61

Возвращаем к жизни Аську (ICQ). Мнение дизайнера  41

От черного прямоугольника в Яндекс.Браузере к ускорению всего Chromium  42

Чем заменить Cisco? Импортозамещение коммутаторов доступа  94

Сколько Google заплатила за потерянный домен google.com  37

Несколько вещей, о которых стоит помнить программисту в возрасте  134

Я веб-разработчик и уже 10 дней не могу написать простейшее приложение  135

Почему я больше не использую MVC-фреймворки  201

Месье, ваши problem solving skills не на высоте, или как я провалил одно собеседование  471

Откуда берутся плохие программисты и в чем их проблема  142

Python и красивые ножки: как я бы знакомил сына с математикой и программированием  65

Взломать PayPal за 73 секунды  30

43 полезных сервиса для управления проектами. Без эпитетов  36

Практические методы самомотивации программиста  98

Вопросы по теме

Yii2: Какое самое популярное расширение для авторизацией с группами пользователей?  0

Как в Yii2 вручную установить расширение, без Composer?  2

Правильно ли реализовал подключение к БД?  0

Как правильно реализовать вложенные запросы?  1

Скрипт для создания музыкального чарта?  0

Комментарии (6)

PQR 1 июня 2015 в 16:04

+2
↑ ↓

Интересно узнать насчёт yield — какие плюсы/удобства получились при его применении? Стал ли код библиотеки компактнее или выросла производительность? Какие были проблемы при переписывании на yield?

Athari 1 июня 2015 в 17:05 (комментарий был изменён)

+3
↑ ↓

Код изменился радикально. Из набора костылей и подпорок вот такого нечитаемого вида:

return new Enumerable(function () use ($self, $inner, $outerKeySelector, $innerKeySelector, $resultSelectorValue, $resultSelectorKey)
{
/** @var $self Enumerable */
/** @var $inner Enumerable */
/** @var $arrIn array */
$itOut = $self->getIterator();
$itOut->rewind();
$lookup = $inner->toLookup($innerKeySelector);
$arrIn = null;
$posIn = 0;
$key = null;

return new Enumerator(function ($yield) use ($itOut, $lookup, &$arrIn, &$posIn, &$key, $outerKeySelector, $resultSelectorValue, $resultSelectorKey)
{
/** @var $itOut \Iterator */
/** @var $lookup \YaLinqo\collections\Lookup */
while ($arrIn === null || $posIn >= count($arrIn)) {
if ($arrIn !== null)
$itOut->next();
if (!$itOut->valid())
return false;
$key = call_user_func($outerKeySelector, $itOut->current(), $itOut->key());
$arrIn = $lookup[$key];
$posIn = 0;
}
$args = array($itOut->current(), $arrIn[$posIn], $key);
$yield(call_user_func_array($resultSelectorValue, $args), call_user_func_array($resultSelectorKey, $args));
$posIn++;
return true;
});
});

код стал вот такой конфеткой:

return new Enumerable(function () use ($inner, $outerKeySelector, $innerKeySelector, $resultSelectorValue, $resultSelectorKey) {
$lookup = $inner->toLookup($innerKeySelector);
foreach ($this as $ok => $ov) {
$key = $outerKeySelector($ov, $ok);
if (isset($lookup[$key]))
foreach ($lookup[$key] as $iv)
yield $resultSelectorKey($ov, $iv, $key) => $resultSelectorValue($ov, $iv, $key);
}
});

В принципе и колбэк лишний можно убрать, но я пока не определился: можно ли полагаться на то, что итератор не будет выполнять дорогие операции в конструкторе?

Скорость не мерял, но с точки зрения количества вызовов мой код был близок к Ginq, то есть скорость возрасла раза в два-три. Когда в язык добавляли yield, тормознутость реализации через итераторы была одним из аргументов (кроме основного — читаемости кода).

Проблем не было, была радость от удаления такого количества мусора. :) Ну и со стопроцентным покрытием тестами нервничать особо не приходилось.

Заодно выпилил к чёртовой бабушке все «коллекции» и прочий хлам и заменил на обычные массивы. Потерялась возможность класть объекты и массивы в ключи, но PHP-программистам такая идея в принципе чужда, поэтому невелика потеря.

PerlPower 1 июня 2015 в 21:53

0
↑ ↓

Вот честно сказать — оба варианта выглядят не очень.

Athari 1 июня 2015 в 22:02

0
↑ ↓

Это одна из самых тяжеловесных функций со сложной логикой. Остальные функции в большинстве своём гораздо проще. Ну вот where в полном составе, например:

public function where ($predicate)
{
$predicate = Utils::createLambda($predicate, 'v,k');
return new Enumerable(function () use ($predicate) {
foreach ($this as $k => $v)
if ($predicate($v, $k))
yield $k => $v;
});
}

(Строчку return new Enumerable(function () use ($predicate) в принципе можно выкинуть. Это наследие от первой версии, надо убедиться, что это не сломает какую-то логику.)

Ну и один из источников сложностей — это что в одной функции приходится рассматривать все варианты, в то время как во многих других языках есть перегрузки методов.

PQR 2 июня 2015 в 14:59

0
↑ ↓

Спасибо за развёрнутый ответ, наглядно!

Возник ещё один теоретический вопрос по генераторам. В PHP 7 планируется поддержка «return» внутри функции-генератора (https://wiki.php.net/rfc/generator-return-expressions) — это как-нибудь пригодится? Видите для себя область применения return?

Athari 2 июня 2015 в 18:46

0
↑ ↓

Мне не повредили бы лямбды (тут всё ясно), extension-методы (чтобы оставить чистые итераторы вместо обёрток), какой-то способ семантично возвращать ключ и значение не виде массива или итератора (чтобы упростить маловменяемые сигнатуры)…

Возвращаемые значения у генераторов — это, по сути, генерация значения + итератора. Единственное место, где такая сущность имеет смысл — это GroupBy, но там нужна независимость значений в паре, поэтому фича не подойдёт даже для упрощения кода на пару строчек.

Судя по спеке, эта штука нужна для сопрограмм в общем виде, а в частном случае генераторов последовательностей пользы мало.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Пометьте топик понятными вам метками, если хотите
Метки лучше разделять запятой. Например: общение, социальные сети, myspace.com, подростки, мердок
или закрыть

Что обсуждают

Сейчас

Вчера

Неделя

Математика на пальцах: линейно-квадратичный регулятор  54

Покопаемся в «режиссёрской версии» Wasteland 2: механики и интерфейсы  24

Дистрибутивы Linux Mint оказались скомпрометированы  10

Представляем ReactOS 0.4  34

API для работы с сервисами: Базовые моменты  2

Идеальный UI фреймворк  77

Обзор IntelliJ IDEA 16 Public Preview  56

Как наши технари в армии служили  56

10 привычек довольного node.js разработчика  48

Покопаемся в «режиссёрской версии» Wasteland 2: механики и интерфейсы  23

Поняв Docker  194

Kotlin 1.0. Задай вопрос команде  121

Чем заменить Cisco? Импортозамещение коммутаторов доступа  94

Идеальный UI фреймворк  77

Новый криптовымогатель «Locky»  67

Месье, ваши problem solving skills не на высоте, или как я провалил одно собеседование  471

Математика на пальцах: методы наименьших квадратов  363

Хабра-граф, -сообщества и куда же делась вся карма  236

Самая базовая потребность: как мы реализовали DNS-хостинг в «Mail.Ru для бизнеса»  203

Почему я больше не использую MVC-фреймворки  201

Интересные публикации

Хабрахабр

Гиктаймс

Мегамозг

Директор ядерного центра пообещал замену ПО SAP и Microsoft  6

Перехватываем всё с помощью CoordinatorLayout Behavior  0

Пишите код, который легко удалять, а не дополнять  7

10 трендов в мире финтеха, которые полезно знать стартапам  0

Удобное преобразование перечислений (enum) в строковые в С++  6

Подпольный рынок кардеров. Перевод книги «KingPIN». Глава 20. «The Starlight Room»  0

Покопаемся в «режиссёрской версии» Wasteland 2: механики и интерфейсы  23

Metronome IM и Jappix: многофункциональный Jabber, без сложностей в настройке  5

Activision Blizzard завершила поглощение King Digital за $5,9 млрд  1

Обзор возможностей Azure Stack Technical Preview 1  3

В EMET обнаружена уязвимость  1

Основы венчурного инвестирования — что нужно знать перед заключением договора с VC  0

Oracle готова заплатить целых $500 миллионов за стартап Ravello Systems  3

Открытые курсы Android StudyJams по всей России  6

Дайджест интересных материалов для мобильного разработчика #141 (15-23 февраля)  0

Как оживить контент если он утратил актуальность  1

Оценки капитализации технологических компаний в 2016 году: конец смутного времени — времени неустойчивого роста  0

Умение говорить «нет» — ключевой навык менеджера. Принципиальное «нет»  3

Gamee — стартап бывшей главы монетизации YouTube  0

Huawei собирается занять первое место на рынке смартфонов  2

Вакансии

Frontend-разработчик
Санкт-Петербург Полный рабочий день от 50 000 до 110 000 руб.

Senior Java developer (Multithreading, Servlets, Spring, Hibernate, Native SQL)
Новосибирск Полный рабочий день от 80 000 до 140 000 руб.

QA Engineer/Тестировщик
Калининград Полный рабочий день от 50 000 руб.

PHP Developer
Томск Полный рабочий день от 70 000 руб.

PHP CRM-разработчик
Москва Полный рабочий день

Создать резюме Разместить вакансию

Заказы

Парсер пабликов в социальной сети Facebook
24.02.2016 0 откликов

Взлом программы (тестирование защиты)
24.02.2016 0 откликов Цена договорная

Переделать дизайн формы заказа
24.02.2016 2 отклика

Доработка проекта на Ruby On Rails
21.02.2016 5 откликов

Сервис по оказанию услуг + магазин
24.02.2016 4 отклика

Зарегистрироваться Разместить заказ

Войти

Регистрация

Разделы

Публикации

Хабы

Компании

Пользователи

Q&A

Песочница

Инфо

О сайте

Правила

Помощь

Соглашение

Услуги

Реклама

Спецпроекты

Тарифы

Контент

Вебинары

Разное

Приложения

Тест-драйв

Помощь стартапам

Работа в IT

© TM

Служба поддержки

Мобильная версия