Читабельный код выполняется медленнее
Недавно я для себя открыл, что PHP не умеет оптимизировать код, а тут новый удар: оказывается, что красота кода отрицательно влияет на производительность.
Как и в прошлый раз, для проверки гипотез использовался Vulcan Logic Disassembler.
Сравнивались два куска кода:
Тест №1:
$a = true;
if (true == $a) {
print 1;
}
?>
и абсолютно аналогичный по функциональности
Тест №2:
$a = true;
if ($a) {
print 1;
}
?>
На выходе получилась такая картина.
Для первого случая:
-------------------------------------------------------------------------------
2 0 FETCH_CONSTANT ~0 'true'
1 ASSIGN $a, ~0
3 2 FETCH_CONSTANT ~2 'true'
3 IS_EQUAL ~3 ~2, $a
4 JMPZ ~3, ->8
4 5 PRINT ~4 1
6 FREE ~4
5 7 JMP ->8
6 8 RETURN 1
9* ZEND_HANDLE_EXCEPTION
Для второго случая:
-------------------------------------------------------------------------------
2 0 FETCH_CONSTANT ~0 'true'
1 ASSIGN $a, ~0
3 2 JMPZ $a, ->6
4 3 PRINT ~2 1
4 FREE ~2
5 5 JMP ->6
6 6 RETURN 1
7* ZEND_HANDLE_EXCEPTION
Видим, что в первом случае PHP, как мы его и попросили, честно сравнил true
c $a
и проверил, равен ли результат сравнения нулю или нет. Во втором же случае PHP просто проверил, равно ли $a
нулю, сэкономив два опкода.
Будучи любопытным, я заменил проверку на равенство true
проверкой на неравенство false
. Хрен редьки не слаще: те же 9 опкодов:
-------------------------------------------------------------------------------
2 0 FETCH_CONSTANT ~0 'true'
1 ASSIGN $a, ~0
3 2 FETCH_CONSTANT ~2 'false'
3 IS_NOT_EQUAL ~3 ~2, $a
4 JMPZ ~3, ->8
4 5 PRINT ~4 1
6 FREE ~4
5 7 JMP ->8
6 8 RETURN 1
9* ZEND_HANDLE_EXCEPTION
Попутно выяснил, что те, кто говорят, что echo
быстрее, чем print
, правы. Я поменял print 1
; на echo 1
, в результате код программы сократился на один опкод:
-------------------------------------------------------------------------------
2 0 FETCH_CONSTANT ~0 'true'
1 ASSIGN $a, ~0
3 2 FETCH_CONSTANT ~2 'false'
3 IS_NOT_EQUAL ~3 ~2, $a
4 JMPZ ~3, ->7
4 5 ECHO 1
5 6 JMP ->7
6 7 RETURN 1
8* ZEND_HANDLE_EXCEPTION
Затем я решил проверить, что лучше: switch
или спагетти из if
/elseif
.
Тест №3
$a = 5;
if ($a == 1) {
echo 1;
}
elseif ($a == 2) {
echo 2;
}
elseif ($a == 3) {
echo 3;
}
else {
echo ":-(";
}
?>
Результат:
-------------------------------------------------------------------------------
2 0 ASSIGN $a, 5
3 1 IS_EQUAL ~1 $a, 1
2 JMPZ ~1, ->5
4 3 ECHO 1
5 4 JMP ->14
6 5 IS_EQUAL ~2 $a, 2
6 JMPZ ~2, ->9
7 7 ECHO 2
8 8 JMP ->14
9 9 IS_EQUAL ~3 $a, 3
10 JMPZ ~3, ->13
10 11 ECHO 3
11 12 JMP ->14
13 13 ECHO ':-('
15 14 RETURN 1
15* ZEND_HANDLE_EXCEPTION
Тест №4
$a = 5;
switch ($a) {
case 1:
echo 1;
break;
case 2:
echo 2;
break;
case 3:
echo 3;
break;
default:
echo ":-(";
break;
}
?>
Результат:
-------------------------------------------------------------------------------
2 0 ASSIGN $a, 5
4 1 CASE ~1 $a, 1
2 JMPZ ~1, ->6
5 3 ECHO 1
6 4 BRK 1
7 5* JMP ->8
6 CASE ~1 $a, 2
7 JMPZ ~1, ->11
8 8 ECHO 2
9 9 BRK 1
10 10* JMP ->13
11 CASE ~1 $a, 3
12 JMPZ ~1, ->16
11 13 ECHO 3
12 14 BRK 1
13 15* JMP ->17
16 JMP ->20
14 17 ECHO ':-('
15 18 BRK 1
16 19* JMP ->21
20 JMP ->17
17 21 RETURN 1
22* ZEND_HANDLE_EXCEPTION
Результат для switch
хоть и более читаемый, но оказался в полтора раза хуже результата для if
. Sad but true.
После этого я собрал (с поддержкой оптимизатора) и поставил eAccelerator и повторил тесты.
Тест №1 и тест №2 дали одинаковый результат:
-------------------------------------------------------------------------------
2 0 ASSIGN $a, true
3 1 JMPZ $a, ->3
4 2 ECHO 1
6 3 RETURN 1
4* ZEND_HANDLE_EXCEPTION
Что характерно, eAccelerator самостоятельно преобразовал print
в echo
. Лапочка.
Для теста №3 результат остался неизменным: там, собственно, нечего оптимизировать.
А результаты теста №4 впечатлили:
4 1 CASE ~0 $a, 1
2 JMPZ ~0, ->5
5 3 ECHO 1
17 4 RETURN 1
7 5 CASE ~0 $a, 2
6 JMPZ ~0, ->9
8 7 ECHO 2
17 8 RETURN 1
10 9 CASE ~0 $a, 3
10 JMPZ ~0, ->13
11 11 ECHO 3
17 12 RETURN 1
14 13 ECHO ':-('
17 14 RETURN 1
15* ZEND_HANDLE_EXCEPTION
На выходе получили результат, который по эффективности аналогичен коду из if
!
Поэтому в заключение скажу: ставьте eAccelerator и будет всем счастье!
Одно обидно: из eAccelerator 0.9.6-rc1 выкинули оптимизатор и кучу всего остального, оставив только кэширование опкода.
Интересно протестировать связку APC + optimizer.
Ну спички же форменные. Да и как это будет сказываться на “производительности” самого интерпретатора?
Спички спичками, но есть несколько нюансов:
Вообще мне на ум приходит сравнение программ, скомпилированных Turbo Pascal 7.0 и Borland C++ 3: в Turbo Pascal оптимизатор отсутствует в принципе, а в BC++ его можно включить. Разница ощутима.
Я натравил оптимизатор на устаревшую локальную копию своего блога (интерпретатор обломался где-то после загрузки плагинов) и сравнил размер выдачи VLD:
Разница (для не полностью загруженного WP) составляет 3,421 опкод. Полагая, что опкод выполняется 0.5 микросекунды (думаю, что для дешевых VPS это где-то рядом), имеем разницу в 1.7 миллисекунды. На полностью загруженном WordPress разница будет видна сильнее. Использование оптимизатора — бесплатный способ снизить нагрузку на сервер.
Положительно. Компиляция и оптимизация будут выполнены один раз, после чего результат будет закэширован в shared memory.