Спотыкаясь на это так нить Я решил написать аналогичный тест на PHP.
Мой тестовый код такой:
// Slow version
$t1 = microtime(true);
for ($n = 0, $i = 0; $i < 20000000; $i++) {
$n += 2 * ($i * $i);
}
$t2 = microtime(true);
echo "n={$n}\n";
// Optimized version
$t3 = microtime(true);
for ($n = 0, $i = 0; $i < 20000000; $i++) {
$n += $i * $i;
}
$n *= 2;
$t4 = microtime(true);
echo "n={$n}\n";
$speedup = round(100 * (($t2 - $t1) - ($t4 - $t3)) / ($t2 - $t1), 0);
echo "speedup: {$speedup}%\n";
2 * ($i * $i)
Версия работает очень похоже, как 2 * $i * $i
,8%
ускорение, когда16%
ускорив. Таким образом, версия PHP получает примерно половину ускорения по сравнению с Java-кодом.Я не буду вдаваться в подробности, но соотношение умножений в оптимизированном и неоптимизированном коде ->
1 суммирование: 3/4
2 суммирования: 4/6
3 суммирования: 5/8
4 суммирования: 6/10
…
И вообще:
где n — количество сумм в цикле. Чтобы быть полезной для нас формулой — нам нужно вычислить предел ее, когда N приближается к бесконечности (чтобы воспроизвести ситуацию, которую мы делаем МНОГО суммирований в цикле). Так :
Таким образом, мы получаем вывод, что в оптимизированном коде должно быть 50% меньше умножений.
Пришло время проанализировать коды операций PHP, которые генерируются интерпретатором PHP. Для этого вам нужно установить Расширение VLD и использовать его из командной строки для генерации операционных кодов php-скрипта под рукой.
$i++
это не то же самое, что ++$i
с точки зрения кодов операций и использования памяти. Заявление $ i ++; генерирует коды операций:POST_INC ~ 4! 1 БЕСПЛАТНО ~ 4
Увеличивает счетчик на 1 и сохраняет предыдущее значение в слоте памяти № 4. Тогда, потому что это значение никогда не используется — освобождает его от памяти. Вопрос — зачем нам хранить ценность, если она никогда не используется?
Изменение POST_INC в ASSIGN_ADD (который не сохраняет дополнительную информацию в памяти) и выполнение развёртывания цикла дает такой тестовый код:
while (true) {
// Slow version
$t1 = microtime(true);
for ($n = 0, $i = 0; $i < 2000; $i+=10) {
// loop unrolling
$n += 2 * (($i+0) * ($i+0));
$n += 2 * (($i+1) * ($i+1));
$n += 2 * (($i+2) * ($i+2));
$n += 2 * (($i+3) * ($i+3));
$n += 2 * (($i+4) * ($i+4));
$n += 2 * (($i+5) * ($i+5));
$n += 2 * (($i+6) * ($i+6));
$n += 2 * (($i+7) * ($i+7));
$n += 2 * (($i+8) * ($i+8));
$n += 2 * (($i+9) * ($i+9));
}
$t2 = microtime(true);
echo "{$n}\n";
// Optimized version
$t3 = microtime(true);
for ($n = 0, $i = 0; $i < 2000; $i+=10) {
// loop unrolling
$n += ($i+0) * ($i+0);
$n += ($i+1) * ($i+1);
$n += ($i+2) * ($i+2);
$n += ($i+3) * ($i+3);
$n += ($i+4) * ($i+4);
$n += ($i+5) * ($i+5);
$n += ($i+6) * ($i+6);
$n += ($i+7) * ($i+7);
$n += ($i+8) * ($i+8);
$n += ($i+9) * ($i+9);
}
$n *= 2;
$t4 = microtime(true);
echo "{$n}\n";
$speedup = round(100 * (($t2 - $t1) - ($t4 - $t3)) / ($t2 - $t1), 0);
$table[$speedup]++;
echo "****************\n";
foreach ($table as $s => $c) {
if ($s >= 0 && $s <= 20)
echo "$s,$c\n";
}
}
Скрипт агрегирует количество попаданий ЦП в одно или другое значение ускорения.
Когда ЦП против Speedup рисуется в виде графика, мы получаем такую картину:
Таким образом, наиболее вероятно, что скрипт получит ускорение на 10%. Это означает, что наши оптимизации привели к +2% ускорение (по сравнению с оригинальными скриптами 8%).
Я почти уверен, что все эти вещи, которые я сделал, могут быть выполнены автоматически JIT’ером PHP. Я не думаю, что трудно автоматически изменить пару кодов операций POST_INC / FREE в один код операции PRE_INC при создании двоичного исполняемого файла. Также не чудо, что PHP JIT’er может применить развертывание цикла. И это только начало оптимизаций!
Надеюсь, что JIT’er будет в PHP 8.0
Других решений пока нет …