Powrót do strony głównej

Opcodes PHP: if vs switch vs match — porównanie

Artykuł porównuje wewnętrzną strukturę konstrukcji rozgałęzień w PHP: if/else, switch i match — przez pryzmat generowanych opcodes. Pokazano, jak kompilator przekształca każdą konstrukcję w instrukcje Zend VM, dlaczego switch na stałych zapewnia wyszukiwanie O(1), a switch(true) niweczy optymalizację, i kiedy match jest preferowany pod względem bezpieczeństwa i czytelności.

if/else vs switch vs match: co dzieje się pod maską w PHP
Advertisement 728x90

Jak PHP kompiluje if/else, switch i match: analiza opcodesów dla programistów

Silnik PHP nie wykonuje kodu źródłowego bezpośrednio — najpierw kompiluje go na sekwencję kodu bajtowego (opcodes), który następnie wykonywany jest przez Zend VM. Wybór między if/else, switch a match wpływa nie tylko na czytelność, ale także na strukturę tej sekwencji: liniowy przeszukiwanie, wyszukiwanie haszowe czy zwracanie bezpośrednio wewnątrz kodu. Dla programistów średniego i wysokiego poziomu rozumienie tych różnic jest kluczowe przy projektowaniu intensywnie obciążonych rozgałęzień, szczególnie w rdzeniu frameworków, routerych oraz logice biznesowej z dziesiątkami warunków.

Dlaczego opcodes to nie abstrakcja, ale wydajność

Opcodes to rzeczywiste instrukcje, które Zend VM przetwarza krok po kroku. Ich liczba, kolejność i rodzaj determinują cykle procesora, przeoczenia pamięci podręcznej oraz przewidywalność rozgałęzień. Na przykład if/else z 10 gałęziami w najgorszym przypadku wykonuje 10 porównań (IS_IDENTICAL) i nawet 10 skoków warunkowych (JMPZ). Każdy taki skok może spowodować mispredykację w procesorze — zwłaszcza przy nieregularnych danych wejściowych. Z kolei switch na konstantach tekstowych wykorzystuje SWITCH_STRING: jedną operację haszową i jeden bezwarunkowy skok (JMP) do odpowiedniej gałęzi. Różnica ta nie jest teoretyczna — mierzona jest w cyklach i widoczna w mikrobenczmarkach przy ponad 10⁶ wywołaniach na sekundę.

Porównanie trzech podejść na poziomie instrukcji

Rozważmy trzy implementacje tego samego algorytmu — przypisywania zniżki w zależności od statusu klienta ('gold', 'silver', 'bronze', default). Wszystkie przykłady zostały wykonane w PHP 8.1 z użyciem phpdbg -p, bez optymalizacji OPcache — czyli pokazano „surowy” wyjście kompilatora.

Google AdInline article slot

if/else: O(N) liniowe przeszukiwanie

Każda gałąź to osobna para porównania i skoku. Nie ma optymalizacji kolejności: kompilator zachowuje pierwotny porządek elseif. Przy $status === 'bronze' wykonuje się trzy IS_IDENTICAL, trzy JMPZ i jeden JMP do przypisania wartości. W najgorszym przypadku — N porównań i N−1 skoków.

switch: hybryda tablicy haszowej i fallback-przeszukiwania

Główną instrukcją jest SWITCH_STRING. Podczas kompilacji PHP buduje tablicę haszową: każdemu literałowi tekstowemu ('gold', 'silver') przypisuje adres docelowej instrukcji. Podczas wykonywania obliczany jest hash $status, a VM natychmiast przechodzi do odpowiedniego bloku. Trudność to amortyzowany O(1). Blok fallback (instrukcje 0002–0008 w oryginalnym kodzie) aktywowany jest tylko w przypadku kolizji hasha lub nieścisłego porównania — w typowym use-case nie jest wykonywany.

match: zwracanie bezpośrednio bez skoków

match to wyrażenie, a nie operator. Jego opcodes nie zawierają JMP ani JMPZ. Kompilator generuje sekwencję IS_IDENTICAL + JMPNZ tylko do sprawdzenia, ale rezultat od razu przypisywany jest przez ASSIGN do celowej zmiennej — bez pośrednich metek i skoków. Dla trzech gałęzi otrzymujemy 3 porównania i 3 przypisania do stosu, jednak ostateczna wartość pobierana jest z pierwszego pasującego =>. To czyni match semantycznie bardziej przewidywalnym i bezpieczniejszym (brak fall-through), ale nie szybszym niż switch przy dużej liczbie gałęzi.

Google AdInline article slot

Kluczowe ograniczenia i pułapki

  • switch(true) niszczy optymalizację: przy case $x > 100 kompilator nie może zbudować tablicy haszowej — powstaje liniowa łańcuch IS_SMALLER + JMPNZ, identyczny jak if/else.
  • Typy kluczy są ważne: SWITCH_LONG działa dla liczb całkowitych, SWITCH_STRING — dla ciągów znaków. Mieszane typy lub obiekty zmuszają silnik do używania fallback.
  • break to JMP: każdy break kompilowany jest jako bezwarunkowy skok (JMP → end). Brak break oznacza, że VM kontynuuje wykonywanie w kolejności — stąd fall-through. To nie „behawior case", a efekt uboczny braku instrukcji.
  • default nie jest specjalną gałęzią: to po prostu metka umieszczona w opcodes po wszystkich case. Jeśli case 'bronze' nie ma break, wykonanie „przepada” do default nie ze względu na logikę, lecz ponieważ instrukcje idą pod rząd.

Co jest ważne

  • if/else skaluje się liniowo: przy 50 gałęziach w najgorszym przypadku — 50 porównań.
  • switch na stałych tekstowych/liczbowych daje O(1) wyszukiwanie dzięki kompilacji tablicy haszowej.
  • match jest bezpieczniejszy i czystszy semantycznie, ale nie szybszy niż switch przy więcej niż 10 gałęziach — nadal sprawdza wartości w kolejności.
  • switch(true) z wyrażeniami w case to antypatrn: to if/else z cukrem syntaktycznym i dodatkowym nakładem.
  • Zagnieżdżone if/else eksponencjalnie zwiększają liczbę skoków: 3 poziomy × 5 gałęzi = do 15 JMPZ i 15 JMP.

Dla specjalistów technicznych: jeśli twój kod zawiera ciągu z więcej niż 7 elseif lub jest używany w hot path (routing, serializacja, walidacja), zamiana na switch z konstantami kluczy daje wymierną korzyść w przepustowości. W PHP 8.0+ match warto stosować tam, gdzie ważna jest bezpieczeństwo i czytelność, a nie maksymalna prędkość.

Optymalizacja powinna zaczynać się od analizy opcodesów — nie od domysłów. Uruchom phpdbg -p script.php na swoim krytycznym fragmencie i policz ilość IS_ i JMP. Jeśli ich jest więcej niż 10 — czas na refaktoring.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Czytaj dalej