Bild: Stefan Keller

PHP: (Um wie viel) Ist Type Hinting langsamer?

PHP ist normalerweise sehr pflegeleicht im Umgang mit Code. Um den Typ einer Variablen musst du dir normalerweise keine Gedanken machen. Seit PHP 7 (und im eingeschränkten Maße schon davon) kannst du dir aber darüber Gedanken machen müssen, genau wie in „richtigen“ Programmiersprachen. Das Ganze nennt sich Typdeklaration (type declaration) und wird umgangssprachlich auch (falsch) als Type Hinting bezeichnet. Aber wie wirkt sich das Ganze auf die Performance aus? Lohnt es sich denn, sich Gedanken über die Variablentypen machen müssen zu wollen?

Was ist Type Hinting/Typdeklaration?

Um die Erbse direkt am Anfang zu zählen: Es gibt einen wesentlichen Unterschied zwischen Type Hinting und Typdeklarationen. Type Hinting ist ein Kommentar, üblicherweise im phpdoc-Stil, der dem Entwickler (oder der IDE) einen Hinweis darauf gibt, welche Typen erwartet werden. Da es sich dabei um einen Kommentar handelt, sind keine Performance-Unterschiede zu erwarten.

Die Typdeklaration hingegen schreibt vor, welche Typen in der Signatur einer Funktion auftauchen dürfen und welche Typen eine Funktion zurückgibt. Hierbei handelt es sich um eine Vorschrift. Wenn die Funktion ein mysqli_result zurückgeben soll, aber tatsächlich ein false kommt, wirft PHP einen Fehler. (NB: Es gibt Typen, die sich problemlos ineinander umwandeln lassen und PHP macht das auch, es sei denn, deine Scripte beginnen mit declare(strict_types = 1); – in dem Fall wird gar nichts automatisch umgewandelt)

PHP ist eine dynamische Sprache, d. h. der Typ von Variablen wird, falls notwendig, nach bestem Wissen und Gewissen dynamisch umgewandelt, damit etwas Kompatibles dabei herauskommt. Wenn die Sprache stattdessen prüfen soll, ob die übermittelten Werte den erwarteten Typ haben, kann man sich vorstellen, dass es diesen Service nicht gratis gibt. Aber wie teuer ist er?

Performance von PHP-Typdeklarationen

Um herauszufinden, wie es um die Performance von PHP mit und ohne Typdeklaration bestellt ist, habe ich ein kleines Script geschrieben. Es besteht aus zwei Funktionen und vier Schleifen. Die Funktionen machen exakt dasselbe und unterscheiden sich nur darin, dass eine die Typen überprüft und eine nicht.

Die Schleifen laufen jeweils von 0 bis 99 und darin jeweils von 0 bis 9.999.999. Dabei wird jeweils die eine oder die andere Funktion aufgerufen und die Zeit gestoppt. Simpel.

<?php

function nohints($a)
{
    return $a + 1;
}

function withhints(int $a): int
{
    return $a + 1;
}

echo 'NO HINTS<br>';
echo '<ul>';
$overall = microtime(true);
for ($i = 0; $i < 100; $i++) {
    $timer = microtime(true);
    for ($j = 0; $j < 10000000; $j++) {
        $x = nohints($i * $j);
    }

    echo '<li>';
    echo $i . ' ::: ' . (microtime(true) - $timer);
    echo '</li>';
}

$overall = (microtime(true) - $overall);
echo '<li>';
echo 'OVERALL ::: ' . $overall;
echo '</li>';

echo '</ul>';

echo '<hr>';

echo 'HINTS<br>';
echo '<ul>';
$overall = microtime(true);
for ($i = 0; $i < 100; $i++) {
    $timer = microtime(true);
    for ($j = 0; $j < 10000000; $j++) {
        $x = withhints($i * $j);
    }

    echo '<li>';
    echo $i . ' ::: ' . (microtime(true) - $timer);
    echo '</li>';
}

$overall = (microtime(true) - $overall);
echo '<li>';
echo 'OVERALL ::: ' . $overall;
echo '</li>';

echo '</ul>';

Dass die Funktionen jeweils eine Milliarde Mal aufgerufen werden, hat den Grund, dass wir sehen wollen, ob es Performance-Implikationen gibt und wie groß sie sind. Dass dies auf 100 Runden aufgeteilt wird, soll Variationen innerhalb einer Applikation aufzeigen. Anhand der Ergebnisse könnte man argumentieren, dass es auch weniger Runden getan hätten, aber wo bleibt da der Spaß?

Jenes Script läuft auf einem Mac Studio M4 Max unter Debian Bookworm mit PHP 8.4.6, innerhalb einer virtuellen Maschine, die auf VMware basiert. Die Ergebnisse sehen wie folgt aus:

NO HINTS
0 ::: 0.10527801513672
1 ::: 0.071072816848755
2 ::: 0.070273160934448
3 ::: 0.070631980895996
4 ::: 0.070239067077637
5 ::: 0.070678949356079
6 ::: 0.070624828338623
7 ::: 0.070699214935303
8 ::: 0.070677042007446
9 ::: 0.07181191444397
10 ::: 0.071404933929443
11 ::: 0.070657968521118
12 ::: 0.070892810821533
13 ::: 0.070219993591309
14 ::: 0.070834875106812
15 ::: 0.070441961288452
16 ::: 0.070380926132202
17 ::: 0.07030987739563
18 ::: 0.070625066757202
19 ::: 0.070321083068848
20 ::: 0.070188999176025
21 ::: 0.07048487663269
22 ::: 0.070487022399902
23 ::: 0.070826053619385
24 ::: 0.069991111755371
25 ::: 0.070058822631836
26 ::: 0.070549011230469
27 ::: 0.070816993713379
28 ::: 0.070188045501709
29 ::: 0.070339918136597
30 ::: 0.070189952850342
31 ::: 0.070162773132324
32 ::: 0.070755004882812
33 ::: 0.07091498374939
34 ::: 0.070455074310303
35 ::: 0.070372104644775
36 ::: 0.070559024810791
37 ::: 0.070795059204102
38 ::: 0.070486068725586
39 ::: 0.070276021957397
40 ::: 0.070236921310425
41 ::: 0.070463895797729
42 ::: 0.069965124130249
43 ::: 0.070539951324463
44 ::: 0.070986032485962
45 ::: 0.070451974868774
46 ::: 0.070827960968018
47 ::: 0.070003032684326
48 ::: 0.070229053497314
49 ::: 0.071011066436768
50 ::: 0.070219039916992
51 ::: 0.070487022399902
52 ::: 0.070879936218262
53 ::: 0.069981098175049
54 ::: 0.070492029190063
55 ::: 0.070236921310425
56 ::: 0.07046103477478
57 ::: 0.070641040802002
58 ::: 0.070708036422729
59 ::: 0.071377992630005
60 ::: 0.070394992828369
61 ::: 0.07082200050354
62 ::: 0.070324897766113
63 ::: 0.070455074310303
64 ::: 0.070257186889648
65 ::: 0.070466041564941
66 ::: 0.070111036300659
67 ::: 0.070219993591309
68 ::: 0.070302963256836
69 ::: 0.07092809677124
70 ::: 0.071099042892456
71 ::: 0.070564985275269
72 ::: 0.072497129440308
73 ::: 0.071696043014526
74 ::: 0.06983208656311
75 ::: 0.070950984954834
76 ::: 0.070465087890625
77 ::: 0.070258855819702
78 ::: 0.070852041244507
79 ::: 0.070293188095093
80 ::: 0.070438861846924
81 ::: 0.070796012878418
82 ::: 0.070066928863525
83 ::: 0.070321083068848
84 ::: 0.070562124252319
85 ::: 0.069991111755371
86 ::: 0.072103023529053
87 ::: 0.070925951004028
88 ::: 0.070840120315552
89 ::: 0.070727825164795
90 ::: 0.070678949356079
91 ::: 0.070430994033813
92 ::: 0.07059907913208
93 ::: 0.071646928787231
94 ::: 0.069941997528076
95 ::: 0.07069206237793
96 ::: 0.070616960525513
97 ::: 0.070122003555298
98 ::: 0.070497989654541
99 ::: 0.070079803466797
OVERALL ::: 7.0921351909637

HINTS
0 ::: 0.089193105697632
1 ::: 0.088748931884766
2 ::: 0.086396932601929
3 ::: 0.086484909057617
4 ::: 0.086230039596558
5 ::: 0.085978031158447
6 ::: 0.086316823959351
7 ::: 0.086275100708008
8 ::: 0.086565017700195
9 ::: 0.086433172225952
10 ::: 0.086297988891602
11 ::: 0.086435079574585
12 ::: 0.087884902954102
13 ::: 0.086260080337524
14 ::: 0.086216926574707
15 ::: 0.086568117141724
16 ::: 0.086067914962769
17 ::: 0.086502075195312
18 ::: 0.086413145065308
19 ::: 0.086124897003174
20 ::: 0.090314149856567
21 ::: 0.08634090423584
22 ::: 0.086432933807373
23 ::: 0.086106777191162
24 ::: 0.086472034454346
25 ::: 0.086004018783569
26 ::: 0.086509943008423
27 ::: 0.087160110473633
28 ::: 0.086086988449097
29 ::: 0.086600065231323
30 ::: 0.086061000823975
31 ::: 0.086570978164673
32 ::: 0.086410999298096
33 ::: 0.086113929748535
34 ::: 0.086143970489502
35 ::: 0.086397171020508
36 ::: 0.0865318775177
37 ::: 0.086057901382446
38 ::: 0.086577177047729
39 ::: 0.090740919113159
40 ::: 0.086547136306763
41 ::: 0.087484121322632
42 ::: 0.085831880569458
43 ::: 0.086374998092651
44 ::: 0.086482048034668
45 ::: 0.086058139801025
46 ::: 0.089055061340332
47 ::: 0.086238861083984
48 ::: 0.086331844329834
49 ::: 0.0887451171875
50 ::: 0.088603973388672
51 ::: 0.092258930206299
52 ::: 0.092604875564575
53 ::: 0.091909885406494
54 ::: 0.092705965042114
55 ::: 0.089514970779419
56 ::: 0.086217880249023
57 ::: 0.086273908615112
58 ::: 0.086570024490356
59 ::: 0.086140155792236
60 ::: 0.086647987365723
61 ::: 0.087548017501831
62 ::: 0.086620807647705
63 ::: 0.085958957672119
64 ::: 0.086488962173462
65 ::: 0.086191177368164
66 ::: 0.086546897888184
67 ::: 0.086449146270752
68 ::: 0.08635401725769
69 ::: 0.086709976196289
70 ::: 0.086344003677368
71 ::: 0.086544990539551
72 ::: 0.086184978485107
73 ::: 0.086289167404175
74 ::: 0.087363004684448
75 ::: 0.086123943328857
76 ::: 0.086430072784424
77 ::: 0.086580038070679
78 ::: 0.090245008468628
79 ::: 0.087490081787109
80 ::: 0.086164951324463
81 ::: 0.086487054824829
82 ::: 0.086265087127686
83 ::: 0.086441993713379
84 ::: 0.086199045181274
85 ::: 0.086432933807373
86 ::: 0.086101055145264
87 ::: 0.086655139923096
88 ::: 0.086397171020508
89 ::: 0.086544990539551
90 ::: 0.086722135543823
91 ::: 0.086115837097168
92 ::: 0.0865478515625
93 ::: 0.086028099060059
94 ::: 0.086388111114502
95 ::: 0.086419820785522
96 ::: 0.086405038833618
97 ::: 0.086308002471924
98 ::: 0.08607292175293
99 ::: 0.087404012680054
OVERALL ::: 8.6943159103394

Interpretation des Benchmarks

Zunächst einmal kann festgehalten werden, dass die Durchgänge (bis auf den allerersten) bemerkenswert konsistent ablaufen. Zehn Millionen Aufrufe ohne Typdeklarationen dauern um die 0,070 Sekunden, während die strengere Variante etwa 0,086 Sekunden benötigt. Insofern: Ja, die Typdeklaration hat das Ganze um coole 23 Prozent verlangsamt. Aber das sind nur die nackten – und vor allem übertrieben dargestellten – Zahlen.

Beim Benchmark haben wir uns ein Szenario gebastelt, das besonders eindrucksvoll zeigt, was wir sehen wollen. Das Script verwendet zweimal eine Milliarde Funktionsaufrufe und sonst nichts weiter. Das ist natürlich praxisfern, aber gleichzeitig auch gewollt, denn wir wollen ja diese isolierte Frage beantwortet haben.

In der Praxis benötigt die Arbeit, die innerhalb von Funktionen verrichtet wird, die meiste Zeit. Wenn eine Datenbank involviert ist, findet sich sogar dort die meiste Wartezeit. Das bedeutet also: Der Check auf korrekte Typen ist um fast ein Viertel langsamer, geht aber trotzdem noch so schnell, dass es keine Rolle spielt.

Limitierungen des Benchmarks

Mir ist bewusst, dass es sich bei dem Benchmark nicht um den bestmöglichen Test handelt. Es geht hier ausschließlich um Integers und da ist keine ausschweifende, schwarze Magie involviert. „Interessanter“ wird es, wenn statt primitiver Typen Klassen und am besten noch mit Vererbung involviert sind. Dann muss der Interpreter überprüfen, ob die Klassen voneinander abstammen und das alles zusammenpasst. Je nach Komplexität kann hierbei noch mehr Rechenzeit auf der Strecke bleiben.

Weiterhin gibt das Script der Sprache die bestmögliche Chance, ihre Stärken auszuspielen. Die Funktionen erwarten Integers und bekommen sie auch. Und zwar ohne Wenn und Aber. Wenn wir das hingegen modifizieren und als Input einen anderen (aber leicht konvertierbaren) Datentyp übergeben, dann passieren interessante Sachen. Einmal habe ich die $i-Schleifen auf 10 limitert, da die Durchläufe so konsistent waren – vor allem aber, weil der zweite Versuch deutlich länger dauert und deshalb das Timeout erreicht wurde (ups!). Wenn wir $i * $j im Schleifenkörper zu einem String casten ((string)$i * $j), dann muss PHP das Ergebnis wieder umwandeln. Und dann sieht das Benchmark-Resultat so aus:

NO HINTS
0 ::: 0.2098867893219
1 ::: 0.17729306221008
2 ::: 0.17834401130676
3 ::: 0.17905807495117
4 ::: 0.17254900932312
5 ::: 0.17361998558044
6 ::: 0.17215991020203
7 ::: 0.17344284057617
8 ::: 0.17210292816162
9 ::: 0.17415904998779
OVERALL ::: 1.7826330661774

HINTS
0 ::: 0.18387413024902
1 ::: 0.18522500991821
2 ::: 0.18353605270386
3 ::: 0.18646097183228
4 ::: 0.18467712402344
5 ::: 0.18418216705322
6 ::: 0.18851613998413
7 ::: 0.18643689155579
8 ::: 0.18391799926758
9 ::: 0.18391394615173
OVERALL ::: 1.8507571220398

Huch? Beträgt der Unterschied jetzt nur noch vier Prozent? Das zeigt, was ich meine: Die Prüfung der Typen ist das geringste Übel. Die Rechenzeit wird bei der eigentlichen Arbeit investiert.

Sollte ich die Typen deklarieren?

Es gibt noch einen Grund, warum du die Typdeklaration verwenden solltest, zumal die Performanceeinbußen zu vernachlässigen sind. Damit lassen sich potenzielle Bugs frühzeitig erkennen und unvorhergesehenes Verhalten im Vorfeld verhindern.

PHP bringt Funktionen mit, die bestimmte Dinge erledigen oder im Fehlerfall false liefern. Wenn du mit diesem Ergebnis weiterarbeitest und beispielsweise in eine Integer-Funktion einspeist, wird aus dem false eine 0. Da eine 0 aber auch ein gültiger Integer sein kann, haben wir den Salat. Wenn du deiner Funktion jedoch sagst, dass sie nur int akzeptieren darf, ist false ein Typkonflikt, den PHP entsprechend frühzeitig mit einem Fehler abwürgt. Dies gilt besonders dann, wenn du den Typ des Rückgabewertes deklarierst (oder strict_types = 1 deklarierst).

Weiterhin sind die Deklarationen nützlich im Umgang mit Schnittstellen und externen Bibliotheken. Denn wenn du selbst eine API bastelst, mit der deine App arbeitet, auf der Basis einer externen Bibliothek, dann kannst du so sicherstellen, dass sich weder deine API noch die Bibliothek in die Quere kommen. Und natürlich gilt auch der Umkehrschluss: Bugs erkennst du schon frühzeitig.

TL;DR?

Ja, Typdeklarationen machen den Umgang mit Funktionen etwas langsamer, aber nicht so, dass es nennenswert wäre. Dafür kommen sie zu selten vor und die Laufzeit eines Scriptes wird wesentlich stärker von anderen Faktoren (wie dem Warten auf die Datenbank) beeinflusst.

Schreibe einen Kommentar