Pull to refresh

Пользуетесь PHP на 64-х битной платформе? Значит потеряли в производительности!

Reading time 4 min
Views 1.7K


По идее, эта статья должна была выйти в блоге «Linux для всех». Однако, данная проблема не проявила себя в аналогичных тестах на python и perl, а следовательно специфично-зависимая от PHP и разрядности платформы. А так PHP у нас прежде всего связан с сайтами, то и блог соответствующий — хостинг.

Внимание! Следующий текст содержит сцены насилия фрагменты исходного кода и может нанести неисправимый урон вашему мозгу и разрушить веру в человечество linux php.

Все началось с жалобы на то, что под 64-х битной архитектурой следующий скрипт на PHP работает катастрофически медленно:
<?php

$cnt=0;
while ($cnt++<5) {

echo '----- TEST #'.$cnt.'-----<br>';

echo 'start FOR & WHILE testing<br>';

$start = microtime(1);
for($i=0;$i<3000000;$i++) {}
$end = microtime(1);
$time = $end - $start;
echo $time.' for($i=0;$i<3000000;$i++) {}<br>';

$start = microtime(1);
$i=0; while($i++<3000001) {}
$end = microtime(1);
$time = $end - $start;
echo $time.' while($i++<3000001) {}<br>';

$start = microtime(1);
$i=0; while($i<3000000) {$i++;}
$end = microtime(1);
$time = $end - $start;
echo $time.' while($i<3000000) {$i++;}<br>';

$start = microtime(1);
$i = 0;
while ($i<3000000): $i++; endwhile;
$end = microtime(1);
$time = $end - $start;
echo $time.' while ($i<3000000): $i++; endwhile;<br>';

echo '<br><br>start a.=b & a=a.b testing<br>';

$a = NULL;
$b = 'Довольно длинная строка, символов эдак 40';
$start = microtime(1);
for($i=0;$i<3000000;$i++) {$a = $a.$b;}
$end = microtime(1);

$time = $end - $start;
echo $time.' for($i=0;$i<3000000;$i++) {$a = $a.$b;}<br>';

$a = NULL;
$b = 'Довольно длинная строка, символов эдак 40';
$start = microtime(1);
for($i=0;$i<3000000;$i++) $a = $a.$b;
$end = microtime(1);
$time = $end - $start;
echo $time.' for($i=0;$i<3000000;$i++) $a = $a.$b;<br>';

$a = NULL;
$b = 'Довольно длинная строка, символов эдак 40';
$start = microtime(1);
for($i=0;$i<3000000;$i++) {$a .= $b;}
$end = microtime(1);
$time = $end - $start;
echo $time.' for($i=0;$i<3000000;$i++) {$a.=$b;}<br>';

$a = NULL;
$b = 'Довольно длинная строка, символов эдак 40';
$start = microtime(1);
for($i=0;$i<3000000;$i++) $a .= $b;
$end = microtime(1);
$time = $end - $start;
echo $time.' for($i=0;$i<3000000;$i++) $a .= $b;<br>';

$a = NULL;
$b = 'Довольно длинная строка, символов эдак 40';
$start = microtime(1);
$i=0; while($i++<3000001) {$a.= $b;}
$end = microtime(1);
$time = $end - $start;
echo $time.' while($i++<3000001) {$a.= $b;}<br>';

$a = NULL;
$b = 'Довольно длинная строка, символов эдак 40';
$start = microtime(1);
$i=0; while($i++<3000001) $a.= $b;
$end = microtime(1);
$time = $end - $start;
echo $time.' $i=0; while($i++<3000001) $a.= $b;<br>';

}

?>


Действительно, такая проблема присутствует в php5.2, что было проверено на различных дистрибутивах. Следовательно — проблема действительно была связана с разрядностью. Однако, испытав аналог подобного кода на других интерпретаторах, было выяснено — подобной проблемы там нет.
Соответственно — это была исключительно проблема самого PHP, которая проявляла себя именно на 64-х битах. Но почему же настолько сильно, тысяча чертей?

Был произведен запуск со strace, который на i386, PAE и x86_64 выдавал различные значения:
  • i386 mmap
  • PAE mremap (что в логично, это mmap с флагами)
  • x86_64 brk и mmap (а вот зачем brk?)


Что же такое этот brk, почему он так влияет на скорость работы php и как избежать подобных ситуаций?
Прежде всего отмечу, что картинка к топику — кликабельная и ведет на фундаментальную статью, которая раскрывает все подробности работы с памятью. Однако она очень сложная и на английском языке. Если кратко — то brk позволяет изменить размер выделенной памяти вместо откусывания куска через mmap, что сокращает ее фрагментацию.

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

Так почему же на i386 мы используем только mmap, в то время как на x86_64 туда проникает brk? Для ответа на этот вопрос обращаемся к первоисточнику — malloc:
Normally, malloc() allocates memory from the heap, and adjusts the size of the
heap as required, using sbrk(2). When allocating blocks of memory larger than
MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory
as a private anonymous mapping using mmap(2). MMAP_THRESHOLD is 128 kB by
default, but is adjustable using mallopt(3).


Теперь все стало на свои места — для x86_64 MMAP_THRESHOLD был увеличен, что и привело к такому интересному результату, связанному по всей видимости с очень хитрым сборщиком мусора в PHP, который в данном случае обхитрил сам себя.

А для того, чтоб вернуть вызов malloc как и в i386, надо вернуть старые значения. Это можно сделать поменять глобально, собрав glibc, что нежелательно или сделать выборочно для процесса, указав в переменных ядра старое значение (если конечно glibc собран с поддержкой этой опции):
export MALLOC_MMAP_THRESHOLD_=131072

Если вставить эту магическую строку в init скрипт apache, то вы можете получить прирост в скорости и больший расход по памяти. Разумеется, это актуально не только для php, но возможно и для многих других программ, когда разработчики вызывая malloc подразумевали mmap.

P.S. Тест лучше запускать с консоли.
strace php -f qq.php даст одну картину
MALLOC_MMAP_THRESHOLD_=131072 strace php -f qq.php даст совершенно другую, конечно если у вас 64 бита.
Tags:
Hubs:
+6
Comments 33
Comments Comments 33

Articles