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-Парсер. Документація не найдетальніша (якщо взагалі є), але зате має симпатичний сайтик. Сама масивна бібліотека з представлених: більше 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 deep arrays
— 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 deep arrays
— 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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.