Zastąpili tracelki TranslatorX64 nową reprezentacją pośrednią HipHop (hhir) i nową warstwą pośrednią, w której rezyduje logika do generowania hhir, do którego faktycznie odnosi się ta sama nazwa, hhir.
Z wysokiego poziomu, używa 6 instrukcji, aby zrobić to, co wymagało wcześniej 9 instrukcji, jak zauważono tutaj: „Zaczyna się od tych samych kontroli typów, ale treść tłumaczenia to 6 instrukcji, znacznie lepiej niż 9 z TranslatorX64”
Jest to głównie artefakt tego, w jaki sposób system został zaprojektowany i jest to coś, co planujemy w końcu posprzątać. Cały kod pozostawiony w TranslatorX64 jest maszyną niezbędną do emitowania kodu i łączenia tłumaczeń; kod, który rozumiał, jak tłumaczyć poszczególne kody bajtowe, zniknął z TranslatorX64.
Kiedy hhir zastąpił TranslatorX64, generował kod, który był około 5% szybszy i wyglądał znacznie lepiej po ręcznej kontroli. Kontynuowaliśmy jego debiut produkcyjny z kolejną mini-blokadą, a do tego dostaliśmy dodatkowe 10% wzrostu wydajności. Aby zobaczyć niektóre z tych ulepszeń w działaniu, spójrzmy na funkcję addPositive i część jej tłumaczenia.
function addPositive($arr) {
$n = count($arr);
$sum = 0;
for ($i = 0; $i < $n; $i++) {
$elem = $arr[$i];
if ($elem > 0) {
$sum = $sum + $elem;
}
}
return $sum;
}
Ta funkcja wygląda jak dużo kodu PHP: zapętla się nad tablicą i robi coś z każdym elementem. Na razie skupmy się na liniach 5 i 6, wraz z ich kodem bajtowym:
$elem = $arr[$i];
if ($elem > 0) {
// line 5
85: CGetM <L:0 EL:3>
98: SetL 4
100: PopC
// line 6
101: Int 0
110: CGetL2 4
112: Gt
113: JmpZ 13 (126)
Te dwie linie ładują element z tablicy, przechowują go w zmiennej lokalnej, a następnie porównują wartość tego lokalnego z 0 i warunkowo skaczą gdzieś na podstawie wyniku. Jeśli interesują Cię bardziej szczegółowe informacje o tym, co dzieje się w kodzie bajtowym, możesz przejrzeć kod bytecode.specification. JIT, zarówno teraz, jak iz powrotem w dniach TranslatorX64, dzieli ten kod na dwa ciągi znaków: jeden z tylko CGetM, a drugi z resztą instrukcji (pełne wyjaśnienie, dlaczego tak się dzieje, nie jest tutaj istotne, ale jest głównie dlatego, że nie wiemy w czasie kompilacji, jaki będzie typ elementu tablicy). Tłumaczenie CGetM sprowadza się do wywołania funkcji pomocniczej C ++ i nie jest bardzo interesujące, więc przyjrzymy się drugiemu tropeletowi. To zatwierdzenie było oficjalną wycofaniem TranslatoraX64,
cmpl $0xa, 0xc(%rbx)
jnz 0x276004b2
cmpl $0xc, -0x44(%rbp)
jnle 0x276004b2
101: SetL 4
103: PopC
movq (%rbx), %rax
movq -0x50(%rbp), %r13
104: Int 0
xor %ecx, %ecx
113: CGetL2 4
mov %rax, %rdx
movl $0xa, -0x44(%rbp)
movq %rax, -0x50(%rbp)
add $0x10, %rbx
cmp %rcx, %rdx
115: Gt
116: JmpZ 13 (129)
jle 0x7608200
Pierwsze cztery wiersze to kontrole typów sprawdzające, czy wartość w $ elem i wartość na górze stosu są oczekiwanymi typami. Jeśli którakolwiek z nich zawiedzie, przejdziemy do kodu, który wyzwala ponowne tłumaczenie traceletu, używając nowych typów do wygenerowania inaczej wyspecjalizowanej części kodu maszynowego. Poniżej znajduje się treść tłumaczenia, a kod ma dużo miejsca na ulepszenia. Na linii 8 znajduje się ciężar własny, łatwo rejestrowalny rejestr rejestrujący ruch na linii 12 oraz możliwość ciągłej propagacji między liniami 10 i 16. Są to wszystkie konsekwencje podejścia bajt-kod-na-raz stosowanego przez TranslatorX64. Żaden szanowany kompilator nigdy nie emitowałby takiego kodu, ale proste optymalizacje wymagane, aby tego uniknąć, po prostu nie pasują do modelu TranslatorX64.
Zobaczmy teraz ten sam tracelet przetłumaczony przy użyciu hhir, przy tej samej wersji hhvm:
cmpl $0xa, 0xc(%rbx)
jnz 0x276004bf
cmpl $0xc, -0x44(%rbp)
jnle 0x276004bf
101: SetL 4
movq (%rbx), %rcx
movl $0xa, -0x44(%rbp)
movq %rcx, -0x50(%rbp)
115: Gt
116: JmpZ 13 (129)
add $0x10, %rbx
cmp $0x0, %rcx
jle 0x76081c0
Zaczyna się od tych samych kontroli typów, ale treść tłumaczenia zawiera 6 instrukcji, znacznie lepiej niż 9 z TranslatorX64. Zauważ, że nie ma żadnych obciążeń ani rejestrów do rejestrowania ruchów, a natychmiastowe 0 z kodu bajtowego Int 0 zostało propagowane w dół do cmp w linii 12. Oto hhir, który został wygenerowany między traceletem a tym tłumaczeniem:
(00) DefLabel
(02) t1:FramePtr = DefFP
(03) t2:StkPtr = DefSP<6> t1:FramePtr
(05) t3:StkPtr = GuardStk<Int,0> t2:StkPtr
(06) GuardLoc<Uncounted,4> t1:FramePtr
(11) t4:Int = LdStack<Int,0> t3:StkPtr
(13) StLoc<4> t1:FramePtr, t4:Int
(27) t10:StkPtr = SpillStack t3:StkPtr, 1
(35) SyncABIRegs t1:FramePtr, t10:StkPtr
(36) ReqBindJmpLte<129,121> t4:Int, 0
Instrukcje kodu bajtowego zostały podzielone na mniejsze, prostsze operacje. Wiele operacji ukrytych w zachowaniu niektórych kodów bajtowych jest jawnie reprezentowanych w hhir, takich jak LdStack w linii 6, która jest częścią SetL. Używając nienazwanych tymczasowych (t1, t2 itd.) Zamiast fizycznych rejestrów do reprezentowania przepływu wartości, możemy łatwo śledzić definicję i użycie każdej wartości. To sprawia, że nie jest łatwo sprawdzić, czy miejsce docelowe obciążenia jest rzeczywiście używane, czy też jedno z danych wejściowych instrukcji jest naprawdę stałą wartością z 3 bajtów temu. Aby uzyskać dokładniejsze wyjaśnienie, czym jest hhir i jak działa, spójrz na specyfikację ir.
Ten przykład pokazał tylko kilka ulepszeń, które hhir wprowadził w stosunku do TranslatorX64. Wdrożenie hhir do produkcji i przejście na emeryturę TranslatorX64 w maju 2013 roku było wielkim kamieniem milowym do osiągnięcia, ale to był dopiero początek. Od tego czasu wdrożyliśmy o wiele więcej optymalizacji, które byłyby prawie niemożliwe w TranslatorX64, dzięki czemu hhvm jest prawie dwukrotnie bardziej wydajny w tym procesie. Było to również kluczowe w naszych staraniach o uruchomienie hhvm na procesorach ARM poprzez izolację i zmniejszenie ilości kodu specyficznego dla architektury, którego potrzebujemy do ponownej implementacji. Po więcej szczegółów zobacz nadchodzący post poświęcony naszemu portowi ARM! ”