You can oppose, that it has no real-life relation. Well, but no abstract piece of code has a real-life relation.
The main idea to assimilate will be seen in the end, when we add abstractions to this code.
function quickSort(array &$a, int $start = 0, int $last = null) {
$wall = $start;
$last = is_null($last) ? count($a) - 1 : $last;
if ($last - $start < 1)
return;
switchValues($a, (int) (($start + $last) / 2), $last);
for ($i = $start; $i < $last; $i++)
if ($a[$i] < $a[$last]) {
switchValues($a, $i, $wall);
$wall++;
}
switchValues($a, $wall, $last);
quickSort($a, $start, $wall - 1);
quickSort($a, $wall + 1, $last);
}
function switchValues(array &$a, int $i1, int $i2) {
if ($i1 !== $i2) {
$temp = $a[$i1];
$a[$i1] = $a[$i2];
$a[$i2] = $temp;
}
}
function generateArr(int $size): array {
$arr = [];
for ($i = 0; $i < $size; $i++) {
$arr[] = (int) (rand() / (1000000000 / $size));
}
return $arr;
}
$size = 1000000;
$arr = generateArr($size);
$start = microtime(true);
quickSort($arr);
$duration = round((microtime(true) - $start) * 1000 * 1000) / 1000;
echo "Sorted $size elements in {$duration}ms\n";
Let's launch it several times and take the average:
PHP 7.4: ~2100 ms
KPHP: ~480 ms (~4x faster)
Let's do the same in C++ and compile with g++ with -Os optimization level (the same as used for KPHP):
#include <iostream>
#include <cstdlib>
#include <chrono>
using std::chrono::steady_clock;
void switchValues(int64_t *, int64_t, int64_t);
void quickSort(int64_t *a, int64_t size, int64_t start = 0, int64_t last = -1) {
int64_t wall = start;
last = last == -1 ? size - 1 : last;
if (last - start < 1)
return;
switchValues(a, (start+last)/2, last);
for (int i = start; i < last; ++i)
if (a[i] < a[last]) {
switchValues(a, i, wall);
wall++;
}
switchValues(a, wall, last);
quickSort(a, size, start, wall-1);
quickSort(a, size, wall+1, last);
}
void switchValues(int64_t *a, int64_t i1, int64_t i2) {
if (i1 != i2)
std::swap(a[i1], a[i2]);
}
int main() {
int64_t size = 1000000;
int64_t *a = new int64_t[size];
for (int64_t i = 0; i < size; ++i)
a[i] = std::rand();
steady_clock::time_point begin = steady_clock::now();
quickSort(a, size);
steady_clock::time_point end = steady_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::milliseconds> (end - begin).count() << " ms\n";
return 0;
}
PHP 7.4: ~2100 ms
KPHP: ~480 ms
C++: ~220 ms
Well, C++ is almost 2x faster than KPHP-compiled. Why, and can we do better in KPHP?
Primarily it's because of the array. KPHP's array behaves like PHP, but in PHP it can be either a hashtable or a vector (with integer indexes 0, 1, 2, … — it's our case).
This means, that reading $a[$i]
for numeric $i in KPHP works like this:
So, reason number one:
int* a
and a[i]
— we did no checks for dimensions, and we know it's a vectorarray $a
and $a[$i]
leads to hashtable/vector checks and dimensions checksNext, look at this code PHP / C++:
// PHP // C++
$temp = $a[$i1]; std::swap(a[i1], a[i2]);
$a[$i1] = $a[$i2];
$a[$i2] = $temp;
In C++, we don't do any checks and just swap two 8-bytes memory pieces: we know, that they don't intersect in memory, that we won't access corrupted memory, etc.
In KPHP, $a[$i] = …
may lead to memory reallocation. For instance, $a[100500] = 0
will convert a vector array to an associative array with 100500 index present, if its length was smaller. That's why $a[$i1] = $a[$i2]
can potentially invoke memory reallocation, and in order to maintain pointers if it occurs, KPHP implicitly inserts an extra variable with explicit copying before assigning. Again, in C++ we just didn't take care of this, as while writing, we knew, that all dimensions are satisfied.
So, reason number two:
swap(a[i1], a[i2])
— direct swapping memory piecesLet's start. We remember, that the bottleneck here is $a[$index]
access.
First, take a look at this:
for ($i = $start; $i < $last; $i++)
if ($a[$i] < $a[$last]) { /* ... */ }
We see, that $a[$last]
is queried for every $i, but we are sure, that in our case $last-th element will remain the same. Let's extract it as a separate variable:
$a_last = $a[$last];
for ($i = $start; $i < $last; $i++)
if ($a[$i] < $a_last) { /* ... */ }
This really speeds up PHP/KPHP, but the same change in C++ does almost nothing because of cheapness.
Second, let's use array_swap_int_keys()
KPHP built-in function:
function switchValues(array &$a, int $i1, int $i2) {
array_swap_int_keys($a, $i1, $i2);
}
This function performs checks only once and performs in-memory swapping for POD types, avoiding checking on every index access and extra code surrounding potential reallocations, as mentioned.
To make it work on PHP, either write a polyfill manually:
#ifndef KPHP
function array_swap_int_keys(array &$a, int $idx1, int $idx2): void {
if ($idx1 != $idx2 && isset($a[$idx1]) && isset($a[$idx2])) {
$tmp = $a[$idx1];
$a[$idx1] = $a[$idx2];
$a[$idx2] = $tmp;
}
}
#endif
Or — better — use existing polyfills convering all KPHP built-ins.
Third, look at array generation:
for ($i = 0; $i < $size; $i++)
$arr[] = (int) (rand() / (1000000000 / $size));
We know exactly, that $size elements would be inserted, and $arr would be a vector. It's a good idea to pre-reserve memory for $size-vector of integers, to avoid reallocations while inserting:
$arr = [];
array_reserve($arr, $size, 0, true);
/* ... */
Let's see what we have achieved by optimizations above:
PHP 7.4: ~2650 ms (was 2100)
KPHP: ~270 ms (was 480)
C++: ~220 ms
KPHP is now almost identical to C++! Wow.
But… PHP became slower?? Why?
In practice, if PHP becomes a bit slower, don’t pay attention to it: PHP for development, KPHP for production.
Remember, that we started using array_swap_int_keys()
from switchValues()
?
function switchValues(array &$a, int $i1, int $i2) {
array_swap_int_keys($a, $i1, $i2);
}
This function is invoked many-many times, and in PHP function calls are quite expensive. If we directly call array_swap_int_keys() without calling switchValues(), it will speed up PHP:
if ($a[$i] < $a_last) {
array_swap_int_keys($a, $i, $wall); // not switchValues()
$wall++;
}
In KPHP, this doesn't matter, as a simple function switchValues() is just inlined and has no overhead.
PHP 7.4: ~1950 ms
KPHP: ~270 ms (~7x faster)
C++: ~220 ms
KPHP works fast here, as it infers $arr to be an array of int. Let's say, generateArr()
will append user input ($argv) to random numbers:
function generateArr(int $size): array {
/* ... */
if (isset($argv[1]))
$arr[] = $argv[1]; // suppose it's a numeric string
}
Then, compile, run, and…
KPHP: ~940 ms (was 270)
What happened? Types have spoilt. $arr was int[], but now it's mixed[], because $argv[$i] is mixed. As a result, quickSort() accepts mixed[] (which holds integers or numeric strings at runtime), and all computations became significantly slower.
How to fix? Convert $argv[1] to integer:
if (isset($argv[1]))
$arr[] = (int)$argv[1];
To prevent such accidents in the future, manually lock return type:
/** @return int[] */
function generateArr(int $size): array { /* ... */ }
If you occasionally spoil types, later on, KPHP will show you an error, thanks to @return.
Always think about types, even if you omit them, because strict typing is performance.
In real life, you use OOP instead of operating data directly.
Let's extend our example. Instead of array $arr
and $arr[$i]
we'll use a wrapping class with getters:
class ArrayHolder {
/** @var int[] */
private $arr;
function __construct(array $arr) {
$this->arr = $arr;
}
function getCount(): int {
return count($this->arr);
}
function getElemAt(int $idx): int {
return $this->arr[$idx];
}
function switchValues(int $idx1, int $idx2) {
array_swap_int_keys($this->arr, $idx1, $idx2);
}
};
Business logic doesn't change pretty much, it just uses another access to data:
function quickSort2(ArrayHolder $a, int $start = 0, int $last = null) {
$wall = $start;
$last = is_null($last) ? $a->getCount() - 1 : $last;
if ($last - $start < 1)
return;
switchValues2($a, (int) (($start + $last) / 2), $last);
$a_last = $a->getElemAt($last);
for ($i = $start; $i < $last; $i++)
if ($a->getElemAt($i) < $a_last) {
switchValues2($a, $i, $wall);
$wall++;
}
switchValues2($a, $wall, $last);
quickSort2($a, $start, $wall - 1);
quickSort2($a, $wall + 1, $last);
}
function switchValues2(ArrayHolder $a, int $i1, int $i2) {
$a->switchValues($i1, $i2);
}
Let's invoke the code above and compare it with accessing an array directly:
PHP 7.4: ~4600 ms (was 2650)
KPHP: ~320 ms (was 270)
Almost nothing changed, but PHP became about 2x slower. That's for the same reason: function calls are expensive in PHP, and fields are more expensive than local variables.
KPHP became a bit slower too. Why? Primarily, because KPHP doesn't track nullability for now, and $o->field
is surrounded with checks that $o is not null. When KPHP is able to detect nullability, even this small performance degradation will vanish.