IPC 2014

Programmiert sauber! Auch für die Performance

PHP ist recht tolerant, was Datentypen angeht – meist werden die Daten so umgewandelt, wie sie gebraucht werden. Das führt aber leider auch zu unsauber programmierten Scripten, die zwar korrekt ausgeführt werden, aber bei denen PHP erst raten muss, was der Programmierer wirklich gemeint hat. Deshalb habe ich mal getestet, inwiefern unsauber programmierte Scripte sich auch auf die Performance auswirken.

Dieser Beitrag wurde inspiriert vom Themenvorschlag von Bill, der sich einige fertige Scripte näher angesehen und festgestellt hat, dass viele unsauber programmiert sind. Insbesondere auffällig sind der Arrayzugriff auf assoziative Arrays, wobei der Arrayschlüssel fälschlicherweise ohne Anführungszeichen angegeben wird.

echo $arr[index];

PHP sucht dann erstmal nach einer Konstante mit dem Namen index. Sollte diese nicht existieren, wandelt es den Token index in einen String um und versucht den Arrayzugriff danach noch einmal. Im Prinzip ist es bei der unsauberen Variante also der doppelte Aufwand.
Nebenbei wird noch ein Fehler vom Typ E_NOTICE ausgegeben, allerdings werden diese oft weder angezeigt noch geloggt.

Um zu testen, wie sehr sich unsaubere Programmierung auf die Performance auswirkt, habe ich folgende beiden Scripte gegeneinander antreten lassen:

// Richtig
$arr = array('farbe'=>'rot','zustand'=>'neu');
echo $arr['farbe'];
 
// Falsch
$arr = array(farbe=>rot,zustand=>neu);
echo $arr[farbe];

Das ganze habe ich jeweils 1000 mal durchlaufen lassen, um einen geeigneten Mittelwert ohne größere Abweichungen zu erhalten. Das Ergebnis:

Test durchschnittliche Laufzeit pro Durchlauf Verhältnis zur schnellsten Variante
Richtig 8.131 ms 100%
Falsch 17.455 ms 215% (+115%)

Die unsaubere Variante benötigt demzufolge mehr als doppelt so viel Zeit wie die saubere. Es ist also sehr zu empfehlen solche "verdeckten" Fehler auszubessern, auch aus Performancegründen.

Weitere Beispiele solcher Fehler sind Funktionsübergaben, wie z.B.

date(d.m.y);
echo Hallo;

Solche Fehler lassen sich übrigens sehr einfach entdecken, ohne dass man jedes Script Zeile für Zeile durchsuchen muss. Einfach das PHP Error-Log in der php.ini aktivieren (log_errors = On) und alle Fehler aufzeichnen (error_reporting = E_ALL). Der Pfad zur Logdatei kann mittels error_log festgelegt werden. Im Log sieht man dann alle Fehler inklusive dem Scriptnamen und der Zeilennummer.

Ein bisschen Off-Topic aber eben doch zum Thema Datentypen möchte ich mich noch über folgendes beschweren: Aus Sicherheitsgründen sollte man ja alle Ein- und Ausgaben filtern. Für numerische Eingaben gibt es da die tolle Funktion ctype_digit, die überprüft, ob die übergebene Zeichenkette ausschließlich aus Ziffern besteht. Dumm nur, dass die Funktion nur funktioniert, wenn man ihr einen String übergibt. Übergibt man z.B. eine int-variable, interpretiert die Funktion die Zahl als ASCII-Code. Deshalb liefert ctype_digit(5) = false und ctype_digit(48) = true (ASCII 48 bis 57 entsprechen 0 bis 9)!
Manche mögen sagen, dann nutzt man eben intval oder is_numeric, allerdings ist das für Postleitzahlen z.B. nicht möglich, da dann bei intval solche, die mit Null beginnen einfach auf 4 Stellen gekürzt werden (aus 01234 wird 1234) und bei is_numeric weitere Eingaben als valide angesehen würden wie +0123.45e6. Also wer die Funktion ctype_digit benutzt, sollte peinlich genau darauf achten, ihr immer Strings zu übergeben oder eine Wrapper-Funktion erstellen, die die Variable in einen String umwandelt und dann ctype_digit aufruft.
Aber das nur am Rande…

Habt ihr noch Beispiele, wo PHP aufgrund seiner Toleranz Datentypen gegenüber Probleme macht bzw. eurer Meinung nach zu tolerant ist?

PS: Übrigens könnt ihr noch bis zum 31.03.2010 (Mittwoch in 2 Tagen) um 16 Uhr am Gewinnspiel für eines der 2 Jahresabos des PHP Magazins teilnehmen.




23 Kommentare bisher »

  1. Sascha Presnac sagt

    am 29. März 2010 @ 07:03

    Zur Filterung von Werten können auch die Filter-Funktionen benutzt werden:
    http://de.php.net/manual/de/ref.filter.php

    Für die int-Probleme gilt dann:
    $iMyInt = filter_var($unsafeint,FILTER_SANITIZE_NUMBER_INT);
    Damit wird alles entfernt, was nicht Zahl ist.

    Sehr praktisch.
    Ansonsten: Super Beitrag!

  2. Andreas sagt

    am 29. März 2010 @ 09:03

    Danke für den Beitrag. Gibt es einen Unterschied zwischen
    $arr = array('farbe'=>'rot','zustand'=>'neu');
    echo $arr['farbe'];
    und
    $arr = array("farbe"=>"rot","zustand"=>"neu");
    echo $arr["farbe"];
    ?

  3. Sascha Presnac sagt

    am 29. März 2010 @ 09:13

    @Andreas: AFAIK versucht PHP, die " zu interpretieren (es könnten ja Variablen drin sein) und braucht dazu länger als/wie mit '. Stand mal irgendwo bei den Google-Sachen, aber ob das wirklich so ist *schulterzuck* Ich könnte mir das schon vorstellen, aber um wieviele Prozentpunkte sich das auswirkt, dass sollte mal jemand untersuchen *winkmitdemzaun*

  4. robo47 sagt

    am 29. März 2010 @ 09:51

    Ich habe mir vor kurzem auf angeschauen welchen Einfluss Notices auf die Performance haben können, dabei führt der Code mit Notices [uninitialisierte variablen oder Array-schlüssel ohne Anführungszeichen/Hochkomma zu einer 5-6 mal so langen ausführzeit bei den betroffenen Stellen:

    http://www.robo47.net/blog/194-How-do-NOTICES-influence-php-scripts-performance

  5. ¨chrisweb sagt

    am 29. März 2010 @ 09:54

    Hab folgenden Code benutzt:

    $measuringTime = $this->_helper->TimeMeasuring;
    $measuringTime->startMeasuring();

    for ($i=0;$i"rot","zustand"=>"neu");

    }

    $time = $measuringTime->stopMeasuring();

    Zend_Debug::dump($time);

    $measuringTime = $this->_helper->TimeMeasuring;
    $measuringTime->startMeasuring();

    for ($i=0;$i'rot','zustand'=>'neu');

    }

    $time = $measuringTime->stopMeasuring();

    Zend_Debug::dump($time);

    Das ergebniss ist:

    array(2) {
    ["rawValue"] => float(8.16702842712)
    ["rawUnit"] => string(12) "Milliseconds"
    }

    array(2) {
    ["rawValue"] => float(7.87496566772)
    ["rawUnit"] => string(12) "Milliseconds"
    }

  6. chrisweb sagt

    am 29. März 2010 @ 09:56

    Hmmm der Code wurde total zestört, hab halt 10000 mal $arr = array("farbe"=>"rot","zustand"=>"neu"); aufgerufen und dann $arr = array('farbe'=>'rot','zustand'=>'neu');. Man sieht dass die zweite Variante schneller ist, aber nicht viel.

  7. chrisweb sagt

    am 29. März 2010 @ 10:00

    Mein Kommentar wurde gelöscht!? Hier nochmal deshalb hier nochmal das Ergebniss:

    10000 mal $arr = array("farbe"=>"rot","zustand"=>"neu");

    array(2) {
    ["rawValue"] => float(8.67700576782)
    ["rawUnit"] => string(12) "Milliseconds"
    }

    10000 mal $arr = array('farbe'=>'rot','zustand'=>'neu');

    array(2) {
    ["rawValue"] => float(7.95006752014)
    ["rawUnit"] => string(12) "Milliseconds"
    }

  8. Michael sagt

    am 29. März 2010 @ 15:02

    Gibt noch ein Problem, dass ich vor einiger Zeit fälschlicher Weise als Bug interpretiert habe: http://blog.xfragger.de/index.php/132/vermeindlicher-php-bug-bei-vergleichoperatoren

    basiert auch auf dem typischen typecasting mist von php ;)

  9. zod sagt

    am 29. März 2010 @ 23:57

    Super Beitrag!

    Hier etwas das mich nervt: Man kann leider ein ArrayObject nicht mit einem einfachen Array vergleichen. Manche mögen es für natürlich halten, aber ich finde das sehr schade und ArrayObject um eine tolle Fähigkeit beschnitten.

    Bsp.:
    $a = array(1,2,3);
    $o = new ArrayObject($a);

    if ($a == $o)
    {
    … // wird niemals eintreten
    }

  10. Sammie sagt

    am 30. März 2010 @ 20:58

    Alle php-internen Funktionen eigenen sich im Prinzip nicht, um festzustellen, ob eine Variable eine Ganzzahl ist. Dazu müsste man die Variable erst in einen bestimmten Type casten.

    ctype_digit ist eben nur ein Check, ob ein String ausschließlich aus Zahlen besteht.. also für Vorwahlen, Postleitzahlen usw bestens geeignet. Aber eben nicht für Intergers gedacht. ctype_digit((string)$var) könnte man jedoch notfalls verwenden.

    Gleiches Problem herrscht bei is_int – nur umgekehrt. Würde man da is_int("123") angeben, wäre das wieder false, weil man eben keine Strings übergeben kann. is_int((int)"123") würde aber gehen. Der Cast in den richtigen Type ist eben entscheidend.

    Man darf eben nie den Denkfehler haben, dass Postleitzahlen wirklich Zahlen sind – es sind Zifferfolgen – aber dennoch Strings. Sobald man solch eine Ziffernfolge in eine Integer umwandeln würde, würde sie ihre führenden Nullen sowieso verlieren. Man kann sie folglich auch nicht in eine INT-Spalte einer DB eintragen und die 0 erhalten (außer mit Zerofill). Beim Auslesen wäre es in dem Fall dann trotzdem ein String. Postleitzahlen sollte man besser gleich als Varchar speichern, denn was anderes sind sie nicht.

    Will man grundsätzlich nur testen, ob eine Variable nur aus Ziffern besteht dann sollte man auf einen Regexp wie zB preg_match("^\d+$",$var) zurückgreifen. Der funktioniert dann unabhängig davon, ob die Variable ein String oder Integer ist.

    Usereingaben, die von Formularen kommen, sind sowieso IMMER Strings (oder Arrays mit Strings). D.h. wenn man wirklich sichergehen will, dass die Usereingabe eine Zahl ist, dann führt kein Weg an einem Cast vorbei.

    $var = intval($var); oder $var = (int)$var;

    Wenn man dann sichergehen will, dass der User etwas valides eingegeben hat, reicht nach dem Cast ein einfachs if($var > 0) im Normalfall aus.

  11. SeeeD sagt

    am 1. April 2010 @ 07:46

    @Sammie:

    Warum sollte man eine Postleitzahl als varchar speichern?
    Ein char(5) bietet sich doch viel eher an (:

  12. Woy sagt

    am 1. April 2010 @ 07:54

    Hallo, vielen Dank für dieses Thema. Das bestätigt doch mal wieder, es ist bessere "sauber" zu bleiben. :)

    Ein Frage noch!!

    Wie macht du die Script Performance Tests?

  13. Sammie sagt

    am 2. April 2010 @ 13:42

    @SeeeD:
    Naja, das hat Vor- und Nachteile. Ich würde eher davon abraten, Char für Postleitzahlen zu verwenden, aber es kommt wohl auf die Daten bzw den Verwendungszweck an. Mit Char funktioniert es zwar gut, wenn du du nur 5 stellige deutsche Postleitzahlen verwendest, sobald du die Datenbank aber noch um schweizer oder österreicher Postleitzahlen erweiterst (die nur 4 stellig sind), dann wärs nicht so gut die in eine Char(5) Spalte zu schreiben.

    Char hat ja immer eine feste Größe und somit die Eigenschaft, die Stellen grundsätzlich mit Leerzeichen nach rechts aufzufüllen, eine "1234" ist in einer Char(5)-Spalte also ein "1234 ". Der einzige "Vorteil" von Char ist der Platzverbrauch. Ein Char(5) verbraucht immer 5 Bytes. Ein Varchar verbraucht 5 Bytes + 1 weiteres zum Speichern der Länge, aber auf die paar Bytes kommts heuztzuage ja wirklich nicht mehr an. Bei einer österreichischen PLZ haben dageben beide 5 Bytes. ;)

    Letztendlich geht beides, aber mir persönlich ists lieber, wenn keine Leerzeichen angehängt werden und die Daten in der DB wirklich die Länge haben, mit der ich sie speichere.

  14. Michael sagt

    am 2. April 2010 @ 15:27

    ein char hat aber peformancebedingt noch einige Vorteile in diesem Fall, welche allerdings wirklich nur im high performancebereich auffallen werden

  15. Sammie sagt

    am 3. April 2010 @ 00:00

    @Michael:

    ja, da hast du natürlich Recht. Gerade bei Updates und vorallem wenn die ganze Tabelle ausschließlich statische Spalten-Typen hat, hat Char klar die Nase vorn, das muss man fairerweise dazu sagen.

  16. Sammie sagt

    am 4. April 2010 @ 19:06

    Da ich grad selbst drüber gestolpert bin, möchte ich noch auf eine weitere Funktion hinweisen, die erst mit PHP5 eingeführt wurde und den meisten von euch wohl eher unbekannt sein dürfte.

    http://www.php.net/manual/de/ref.filter.php

    Beispiel:
    $var = 3;
    print filter_var($var, FILTER_VALIDATE_INT);

    Gibt 3 zurück wenn die Variable eine Zahl ist oder false im Fehlerfall. Auch "3" (als String) funktioniert und damit hätten wir erstmals eine PHP Funktion die kein Cast voraussetzt.

    Über die Filter-Funktionen lassen sich auch viele andere Dinge validieren, wie Strings, Emails, IPs, URLs und vieles mehr. Für Leute, die mit Regexp auf Kriegsfuß stehen, sicher eine interessante Funktion. Im Generellen bietet der klassische Regexp aber oftmals die kompaktere Schreibweise.

  17. frank sagt

    am 6. April 2010 @ 15:36

    um ids sicher für die datenbank zu machen ist auf jeden fall (int)$val die schnellste variante. da kann man dann auch auf die escape_string funktion verzichten.
    filter_var scheitert bei zb. solchen strings wie "1234abc" was ich als einen nachteil ansehe.

    benchmark mit 10mio interationen:
    3,186361 (int)$val
    6,472608 intval($val)
    10,794286 filter_var($val, FILTER_VALIDATE_INT)

    regex hab ich hier jetzt nicht getestet da man davon ausgehen kann das es sehr viel langsamer ist als die ersten 2 varianten.

    strings die in " stehen sind tatsächlich schneller, vorallem wenn noch variablen mit im spiel sind.
    getestet hab ich mit
    $a = 'bla';
    for($i = 0; $i < 10000000; $i++) $b = 'bla '.$a.' blubb';
    und
    for($i = 0; $i < 10000000; $i++) $b = "bla '.$a.' blubb";
    resultat:
    3,61346 string mit ''
    4,508264 string mit ""

  18. Linkhub – Woche 13-2010 - pehbehbeh sagt

    am 7. April 2010 @ 11:35

    [...] Programmiert sauber! – Auch für die Performance. [...]

  19. Sammie sagt

    am 8. April 2010 @ 15:48

    @frank:

    Du kannst nicht Typenumwandlungsfunktionen (int/intval) mit Matchfunktionen (filtervar, preg_match) vergleichen. Äpfel und Birnen unso, weißt schon ;)

    Mir gings ja auch nicht um die Typen-Umwandlung zur Vorbereitung einer DB-Eintragung, sondern zur generellen Prüfung, ob der Variableninhalt ausschließlich aus Ziffern besteht oder nicht.

    Es kann ja durchaus sein, dass man als Programmierer vorher prüfen will, ob wirklich "1234" (also eine reine Ziffernfolge) oder "1234abcd" übergeben wurde, um evtl eine Fehlermeldung auszugeben und dafür braucht man nunmal Matchfunktionen.

    Hab vorhin auch mal einen Performancetest gemacht.
    Alle Ergebnisse sind Durchschnittswerte aus 10 Durchläufen.

    filter_var:
    ———–
    for($i=1;$i<=10000000;$i++)
    {
    filter_var($i, FILTER_VALIDATE_INT);
    }
    Ergebnis: 7.286274 Sekunden

    ctype_digit mit (string)-Cast:
    ————
    for($i=1;$i<=10000000;$i++)
    {
    $i = (string)($i);
    ctype_digit($i);
    }
    Ergebnis: 7.414834 Sekunden

    ctype_digit mit strval()-Cast:
    ————
    for($i=1;$i<=10000000;$i++)
    {
    $i = strval($i);
    ctype_digit($i);
    }
    Ergebnis: 9.409729 Sekunden

    preg_match:
    ———–
    for($i=1;$i<=10000000;$i++)
    {
    preg_match("/^[0-9]+$/",$i);
    }
    Ergebnis: 20.32127 Sekunden

    Wer also PHP 5.2+ installiert hat und auf Ziffern prüfen will, kann also die neue filter_var-Funktion guten Herzens nutzen, da sie im Test am besten abschneidet, einfach lesbar ist und man sich vorherige Typenumwandlung sparen kann.

    Für alle anderen ist ctype_digit immer noch ein guter Ersatz, da er praktisch die gleiche Performance mit sich bringt.

  20. Bill sagt

    am 19. Mai 2010 @ 02:08

    Vielen Dank, so habe ich mir das vorgestellt.

    Das Ergebnis ist auch wirklich sehr interessant.
    Saubere Programmierung zahlt sich aus.

    Viele Grüße.

    Bill

  21. Linkpool Nummer 8 | PHP Gangsta - Der PHP Blog sagt

    am 6. Juni 2010 @ 11:08

    [...] sag es ja immer wieder: Auch NOTICEs sollten sofort behoben werden: http://phpperformance.de/programmiert-sauber-auch-fuer-die-performance/ var flattr_wp_ver = '0.9.5'; var flattr_uid = '13878'; var flattr_url = [...]

  22. anderer jan sagt

    am 12. Juli 2010 @ 01:27

    at frank:
    da musst du aufpassen mit dem vergleich…du verwendest bei den doublequotes auch mehr zeichen…zwar macht das in dem rahmen nur einen geringen unterschied aus, aber ist im großen sicher nicht zu unterschätzen

    aber ja…es sollte selbstverständlich sein, dass singlequotes schneller sind. das folgt schon aus der reinen überlegung,dass die strings in singlequotes nur weitergereicht werden, während strings in doublequotes erst geparsed werden müssen.
    bei variablen ist es noch krasser, da dann nicht nur gesucht sondern auch ersetzt wird, was nochmal ausführungszeit kostet.
    eine gute frage ist noch, wie er concenated strings behandelt…
    aber theoretisch nach deinem benchmark zu urteilen hätten rein performance-technisch die doublequotes komplett (bis auf hidden charakter) ausgedient, oder seh ich das grad falsch?

  23. PHP Benchmark: echo | casibus sagt

    am 7. August 2010 @ 05:59

    [...] habe ich mal der Vollständigkeit halber probiert, was schon von Jan im PHP-Perfomance Bolg angesprochen [...]

Komentar RSS · TrackBack URI

Hinterlasse einen Kommentar

Name: (erforderlich)

eMail: (erforderlich)

Website:

Kommentar: