Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

HipHop for PHP: Test wydajności – rewanż PHP

W poprzednim artykule opisałem wydajność kompilatora HipHop for PHP przedstawiając wyniki sześciu testów PHP zaczerpniętych z serwisu shootout.alioth.debian.org. Nie był to jednak kompletny zestaw skryptów możliwych do zbadania.

Wspomniane testy zostały wtedy wyselekcjonowane w taki sposób, by uniknąć potencjalnych niekompatybilności związanych z kompilacją skryptu PHP do języka C++, które zostały opisane w dokumentacji HipHop for PHP. W przypadku wspomnianych skryptów problematyczne okazało się pobieranie danych wejściowych ze strumienia STDIN (testy: reverse-complement, regex-dna, k-nucleotide), oraz brak wsparcia dla biblioteki gmp (test pidigits).

Zgodnie z prośbą czytelników przedstawiam jednak wyniki dwóch kolejnych testów: regex-dna oraz k-nucleotide.

Uwagi wstępne

Niestety nie udało mi się przeprowadzić testu reverse-complement, ponieważ wystąpiły błędy typu segmentation fault podczas próby uruchomienia skompilowanego skryptu. W wolnej chwili spróbuję zbadać co jest przyczyną tego zachowania.

Poniższych wyników nie dołączam do artykułu „HipHop for PHP: Benchmark”, ponieważ wg dokumentacji projektu HipHop for PHP skrypty testowe korzystają z mechanizmów nie wspieranych przez kompilator.

Platforma testowa

Procesor: Intel(R) Core(TM)2 Duo CPU E7600 @ 3.06GHz
Pamięć: 2,5GB RAM
System: Fedora 12 (64bit)
Kernel: 2.6.32.26-175.fc12.x86_64 #1 SMP

Maszyna była wykorzystywana wyłącznie do testów – w tle działały tylko usługi związane z przeprowadzanymi benchmarkami.

Wyniki testów

W przeciwieństwie do testów (przeważnie matematycznych) z poprzedniego artykułu, poniższe skrypty operują na dużych ilościach danych tekstowych pobieranych z dysku (>250MB), postanowiłem, że pliki z wejściem/wyjściem skryptu PHP będą przechowywane w ramdysku:

[root@localhost ~]# mount -t tmpfs none /var/ramdisk -o size=900m

Test nr 1: regex-dna (Match DNA 8-mers and substitute nucleotides for IUB code)

Kod pliku źródłowego w PHP:
http://shootout.alioth.debian.org/u32/program.php?test=regexdna&lang=php&id=2

Wyniki w PHP:

php -n -d memory_limit=2512M dna.php 0 < /var/ramdisk/out5.txt
agggtaaa|tttaccct 356
[cgt]gggtaaa|tttaccc[acg] 1250
a[act]ggtaaa|tttacc[agt]t 4252
ag[act]gtaaa|tttac[agt]ct 2894
agg[act]taaa|ttta[agt]cct 5435
aggg[acg]aaa|ttt[cgt]ccct 1537
agggt[cgt]aa|tt[acg]accct 1431
agggta[cgt]a|t[acg]taccct 1608
agggtaa[cgt]|[acg]ttaccct 2178
 
50833429
50000017
66800253
DONE IN 34.079181

Oraz wynik w HipHop for PHP:

[root@localhost src]# /tmp/hphp_Cv1BnI/program 0 < /var/ramdisk/out5.txt
agggtaaa|tttaccct 356
[cgt]gggtaaa|tttaccc[acg] 1250
a[act]ggtaaa|tttacc[agt]t 4252
ag[act]gtaaa|tttac[agt]ct 2894
agg[act]taaa|ttta[agt]cct 5435
aggg[acg]aaa|ttt[cgt]ccct 1537
agggt[cgt]aa|tt[acg]accct 1431
agggta[cgt]a|t[acg]taccct 1608
agggtaa[cgt]|[acg]ttaccct 2178
 
50833429
50000017
66800253
DONE IN 33.682081

Jak widać różnica w testach jest marginalna i podpada nawet pod błąd pomiarowy. Czemu w tym przypadku skompilowany skrypt PHP jest tak samo szybki jak tradycyjny? Odpowiedź można znaleźć w kodzie skryptu: twórcy tego kodu PHP posłużyli się bardzo prostym trickiem przyspieszającym działanie programu: do operacji na stringach użyli wyrażeń regularnych (preg_replace). Biblioteka wyrażeń regularnych w obu przypadkach jest już napisana w języku C++, przez co skompilowanie skryptu PHP przyniosło tylko minimalne różnice, które widoczne są w pętlach uruchamiających funkcję preg_replace().

Jedynym plusem kompilacji okazało się w tym przypadku mniejsze zużycie pamięci. O ile HipHop for PHP zużył więcej pamięci typu virtual, to dzięki współdzieleniu bibliotek systemowych prawdziwych zasobów zabrał o 40% mniej niż zwykłe PHP:

                  VIRT   RES   SHR
PHP:              306m  100m  3048
HipHop for PHP:   427m   62m  9.9m

Test nr 2: k-nucleotide (Repeatedly update hashtables and k-nucleotide strings)

Kod pliku źródłowego w PHP:
http://shootout.alioth.debian.org/u32/program.php?test=knucleotide&lang=php&id=3

Wyniki w PHP:

[root@localhost src]# php -n -d memory_limit=2500M rev.php < /var/ramdisk/out.txt > /var/ramdisk/ret0.txt
 
DONE IN 11.481418

Oraz wynik w HipHop for PHP:

[root@localhost src]# /tmp/hphp_VLLBjz/program < /var/ramdisk/out.txt > /var/ramdisk/ret0.txt
 
DONE IN 125.345383

Tutaj pierwszym napotkanym problemem okazał się odczyt ze strumienia STDIN, najszybszy dostępny skrypt PHP na stronie shootout okazał się niekompatybilny z HipHop for PHP (dopiero odpalenie skryptu numer 2 zakończyło się sukcesem).

Najbardziej szokujące jest jednak coś innego, to pierwszy test w którym tradycyjne PHP jest szybsze od HipHop for PHP i to o ponad 1100%.

Nie jest to pomyłka, test powtarzałem kilkanaście razy - różnica utrzymywała się na tym samym poziomie.

Jak to możliwe? Pewną wskazówkę daje nam systemowe polecenie time, oto jej wynik:

real    2m5.345s
user    0m7.764s
sys     1m57.552s

W oczy rzuca się od razu olbrzymia ilość czasu systemowego. Spróbujmy więc przyjrzeć się temu jeszcze dokładniej poleceniem strace:

[root@localhost src]# strace -o debug.txt /tmp/hphp_VLLBjz/program < /var/ramdisk/out.txt  > /var/ramdisk/ret0.txt 2

Oto najciekawsza część wyniku jaki otrzymałem:

[...]
read(0, "CATGGTGAAACCCCGTCTCTACTAAA\nAATAC"..., 8192) = 8192
read(0, "TAAAAATA\nCAAAAATTAGCCGGGCGTGGTGG"..., 8192) = 8192
read(0, "GGCGTGGTGGCGCGCGCCTGTAATCCCAGCTA"..., 8192) = 8192
read(0, "ATCCCAGCTACTCGGGAGGCTGAGGCAGGAGA"..., 8192) = 8192
read(0, "AGGCAGGAGAATCGC\nTTGAACCCGGGAGGCG"..., 8192) = 8192
read(0, "CCGGGAGGCGGAGGTTGCAGTGAGCCGAGATC"..., 8192) = 8192
read(0, "AGCCGAGATCGCGCCACTGCACTCCAGCCTGG"..., 8192) = 8192
read(0, "TCCAGCCTGGGCGACAGAGCGA\nGACTCCGTC"..., 8192) = 8192
mmap(NULL, 847872, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe849495000
mremap(0x7fe849495000, 847872, 851968, MREMAP_MAYMOVE) = 0x7fe8493c5000
mremap(0x7fe8493c5000, 851968, 856064, MREMAP_MAYMOVE) = 0x7fe8493c5000
read(0, "GACT\nCCGTCTCAAAAAGGCCGGGCGCGGTGG"..., 8192) = 8192
mremap(0x7fe8493c5000, 856064, 860160, MREMAP_MAYMOVE) = 0x7fe8493c5000
mremap(0x7fe8493c5000, 860160, 864256, MREMAP_MAYMOVE) = 0x7fe8493c5000
read(0, "GGCGCGGTGGCTCACGCCTGTAATCCCAGCAC"..., 8192) = 8192
mremap(0x7fe8493c5000, 864256, 868352, MREMAP_MAYMOVE) = 0x7fe8493c5000
mremap(0x7fe8493c5000, 868352, 872448, MREMAP_MAYMOVE) = 0x7fe8493c5000
read(0, "ATCCCAGCACTTTGGGAGGCCGAGGCGGG\nCG"..., 8192) = 8192
[...]

Początek logu jest nieciekawy. Dopiero w powyższym wycinku zaczyna się wszystko wyjaśniać. HipHop for PHP po odczytaniu początku strumienia STDIN zaczyna masowo powiększać ilość zaalokowanej pamięci w celu konkatenacji stringu z wynikiem.

Ilość operacji mremap jest przy tym najprawdę olbrzymia (dwa remapowania po 4kB na jeden odczyt danych o wielkości 8kB), co skutecznie obciąża system operacyjny.

Dla porównania podaję log ze zwykłego skryptu PHP:

[...]
mremap(0x7f5821b62000, 24121344, 24383488, MREMAP_MAYMOVE) = 0x7f5821b62000
read(0, "GTGAAACCCCGTCTCTACTAAAAATACAAAAA"..., 8192) = 8192
read(0, "AATACAAAAATTAGCCGGGCGTGGTGGCGCGC"..., 8192) = 8192
read(0, "GGTGGCGCGCGCCTGTA\nATCCCAGCTACTCG"..., 8192) = 8192
read(0, "CAGCTACTCGGGAGGCTGAGGCAGGAGAATCG"..., 8192) = 8192
read(0, "AGGAGAATCGCTTGAACCCGGGAGGCGGAGGT"..., 8192) = 8192
read(0, "AGGCGGAGGTTGCAGTGAGCCGAG\nATCGCGC"..., 8192) = 8192
read(0, "AGATCG\nCGCCACTGCACTCCAGCCTGGGCGA"..., 8192) = 8192
read(0, "GCCTGGGCGACAGAGCGAGACTCCGTCTCAAA"..., 8192) = 8192
read(0, "CCGTCTCAAAAAGGCCGGGCGCGGTGGCTCA\n"..., 8192) = 8192
read(0, "GGTGGCTCACGCC\nTGTAATCCCAGCACTTTG"..., 8192) = 8192
read(0, "CAGCACTTTGGGAGGCCGAGGCGGGCGGATCA"..., 8192) = 8192
[...]

W tym przypadku stosunek operacji remapowania pamięci do ilości odczytów strumienia jest znacznie mniejszy (jedno remapowanie o wielkości 256kB na 32 odczyty danych po 8kB każdy).

Czyżby HipHop for PHP miał problemy z podstawowymi operacjami na stringach?

Jeśli tak, to odpowiedzi udzieli nam pewna programistyczna sztuczka. Postanowiłem przyjrzeć się temu problemowi pisząc dwa proste skrypty, który nie korzystają ze strumienia STDIN, lecz używają wyłącznie funkcjonalności wspieranych przez kompilator HipHop for PHP:

Plik sb-concat.php:

< ?php
 
$result = "";
$max = 160000000;
 
$start = microtime(true);
 
for($i = 0; $i < $max; $i++) {
    $result.= "a";
}
 
printf("GENERATED %d BYTES OF DATA IN %f SEC\n", strlen($result), microtime(true) - $start);

Oraz plik sb-malloc.php:

< ?php
 
$max = 160000000;
$result = str_pad("", $max, chr(0));
$start = microtime(true);
 
for($i = 0; $i < $max; $i++) {
    $result[$i] = "a";
}
 
printf("GENERATED %d BYTES OF DATA IN %f SEC\n", strlen($result), microtime(true) - $start);

Skrypty te bazują po części na moim wcześniejszym artykule dotyczącym możliwości zaimplementowania klasy StringBuilder w języku PHP.

Oto wyniki pliku sb-concat.php:

[root@localhost src]# /tmp/hphp_s1nTpN/program
GENERATED 160000000 BYTES OF DATA IN 63.566147 SEC

Oraz wynik skryptu sb-malloc.php symulujący cząstkową funkcjonalność StringBuilder'a:

[root@localhost src]# /tmp/hphp_smmJbC/program
GENERATED 160000000 BYTES OF DATA IN 7.079259 SEC

Moje obawy zaczynają się potwierdzać. Na przedstawionych wynikach widać, że różnica między dwoma podobnymi, skompilowanymi przez HipHop for PHP skryptami wynosi prawie 900%.

Kody obu testów są niemal identyczne, jedyne czym się różnią to fakt, że w przypadku pliku sb-malloc.php alokujemy 160Mb pamięci już na starcie programu. W drugim skrypcie rozmiar pamięci jest powiększany dynamicznie podczas operacji konkatenacji stringu.

Dla pewności sprawdzimy statystyki systemowe:

[root@localhost src]# time /tmp/hphp_s1nTpN/program
GENERATED 160000000 BYTES OF DATA IN 61.368424 SEC
real    1m1.520s
user    0m7.492s
sys     0m54.005s

Nasze podejrzenia okazały się słuszne. Wraz z mechanizmem kompilacji znanym z takich języków jak C, C++ czy C#, w PHP pojawił się też związany z nimi efekt uboczny - powolna konkatenacja stringów, oraz konieczność korzystania z odpowiednika klasy StringBuilder!.

Warto przy tym wspomnieć, że czasy uzyskiwane przez skompilowany skrypt sb-concat.php były wysoce niestabilne i wahały się pomiędzy 33 sekundami, do ponad 120 sekund (pozostając średnio w okolicy 60 sekund). Prawdopodobną przyczyną tego stanu rzeczy jest rzadko spotykana w Linuxach... fragmentacja pamięci (spowodowana olbrzymią ilością remapingu pamięci).

Jest to olbrzymi problem dla kompilatora HipHop, szczególnie że język PHP słynie z bardzo szybkich operacji na stringach, tak bardzo przydatnych podczas dynamicznego generowania stron WWW:

[root@localhost src]# php -d memory_limit=1024M sb-concat.php
GENERATED 160000000 BYTES OF DATA IN 11.122325 SEC
[root@localhost src]# php -d memory_limit=1024M sb-malloc.php
GENERATED 160000000 BYTES OF DATA IN 10.678082 SEC

Powyższy przykład pokazuje potęgę języka PHP: jego doskonałą wydajność w warstwie prezentacji tekstu (np. generowania kodu HTML na podstawie informacji pobranych z baz danych).

Podsumowanie

Oto zestawienie porównawcze wyników:

Nazwa testu PHP HipHop Proporcje
regex-dna 34.079181 33.682081 101.17%
k-nucleotide 11.481418 125.345383 9.15%
concat 11.122325 63.566147 18.12%
stringbuilder 10.678082 7.079259 150.84%

HipHop for PHP benchmark #2

HipHop for PHP nie nadaje się do stron WWW?

W pierwszym artykule przedstawiłem mocne strony kompilatora HipHop for PHP: jego pozytywny wpływ na szybkość: funkcji, pętli, operacji matematycznych oraz samej składni języka PHP. Daje to możliwość użycia języka PHP w innych dziedzinach programistycznych, nie koniecznie związanych ze światem WWW.

PHP jednak powstał z myślą o generowaniu stron internetowych, z tą myślą został też udoskonalony przez twórców Facebooka.

Czy po tych zmianach PHP jest jeszcze lepszy niż dotychczas? Okazuje się, że niestety nie. Główną domeną PHP jest warstwa prezentacji stron internetowych, czyli generowanie kodu HTML, obsługa zapytań HTTP oraz komunikacja z bazą. Ze względów technicznych HipHop for PHP nie ma wpływu na szybkość bazy danych, a podstawowe zadania jakimi musi się zajmować (czyli operacje na stringach) są przez niego wykonywane do 10 razy wolniej, niż w przypadku zwykłego interpretowanego skryptu.

Tagi: , , , ,

Dodaj odpowiedź