W jaki sposób maszyna wirtualna Hip Hop (HHVM) teoretycznie poprawia wydajność środowiska wykonawczego PHP?

9

Z wysokiego poziomu, jak Facebook, i in. Czy chcesz poprawić wydajność PHP za pomocą wirtualnej maszyny Hip Hop?

Czym różni się od wykonywania kodu przy użyciu tradycyjnego silnika Zend? Czy dlatego, że typy są opcjonalnie definiowane za pomocą hacka, który pozwala na techniki wstępnej optymalizacji?

Moja ciekawość zrodziła się po przeczytaniu tego artykułu, adopcji HHVM .

chrisjlee
źródło

Odpowiedzi:

7

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”

http://hhvm.com/blog/2027/faster-and-cheaper-the-evolution-of-the-hhvm-jit

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! ”

Paul W.
źródło
1

W skrócie: starają się zminimalizować losowy dostęp do pamięci i przeskakują między fragmentami kodu w pamięci, aby ładnie grać z pamięcią podręczną procesora.

Według HHVM Performance Status zoptymalizowali najczęściej używane typy danych, czyli ciągi i tablice, aby zminimalizować losowy dostęp do pamięci. Chodzi o to, aby elementy danych były używane razem (jak elementy w tablicy) tak blisko siebie, jak to możliwe, w pamięci, najlepiej w sposób liniowy. W ten sposób, jeśli dane mieszczą się w pamięci podręcznej procesora L2 / L3, można je przetwarzać o rząd wielkości szybciej niż w pamięci RAM.

Inną wspomnianą techniką jest kompilowanie najczęściej używanych ścieżek w kodzie w taki sposób, aby skompilowana wersja była tak liniowa (ei ma najmniejszą liczbę „skoków”), jak to możliwe, i ładowała dane do / z pamięci tak rzadko, jak to możliwe.

scriptin
źródło