Parsing-Funktionsaufrufe in PHP

Ursprünglicher Autor: Julien Pauli
  • Übersetzung
In diesem Beitrag geht es um die Optimierung von PHP mithilfe des Blackfire-Profilers in einem PHP-Skript. Der folgende Text ist eine detaillierte technische Erklärung des Blackfire-Blog-Artikels .

Die strlen-Methode wird normalerweise verwendet:

if (strlen($name) > 49) {
...
}

Diese Option ist jedoch etwa 20% langsamer als diese:

if (isset($name[49])) {
...
}

Es sieht gut aus. Sicherlich sind Sie dabei, Ihre Quellen zu öffnen und alle strlen () -Aufrufe durch isset () zu ersetzen . Wenn Sie jedoch den Originalartikel sorgfältig lesen , können Sie feststellen, dass der Grund für den 20-prozentigen Leistungsunterschied mehrere Aufrufe von strlen () in der Größenordnung von 60 bis 80.000 Iterationen sind.

Warum?


Es geht nicht darum, wie strlen () die Länge von Strings in PHP berechnet, da alle zum Zeitpunkt des Aufrufs dieser Methode bereits bekannt sind. Wann immer möglich, werden die meisten zur Kompilierungszeit berechnet. Die Länge der an den Speicher gesendeten PHP-Zeichenfolge ist in einer C-Struktur eingekapselt, die diese Zeichenfolge enthält. Daher liest strlen () diese Informationen einfach und gibt sie unverändert zurück. Dies ist wahrscheinlich die schnellste der PHP-Funktionen, da sie überhaupt nichts berechnet. Hier ist der Quellcode:

ZEND_FUNCTION(strlen)
{
    char *s1;
    int s1_len;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s1, &s1_len) == FAILURE) {
        return;
    }
    RETVAL_LONG(s1_len);
}

Da isset () keine Funktion ist, liegt der Grund für den 20-prozentigen Leistungsverlust in strlen () hauptsächlich in den damit verbundenen Verzögerungen beim Aufrufen der Funktion in der Zend-Engine.

Es gibt noch eine Sache: Wenn Sie die Leistung von strlen () mit etwas anderem vergleichen, wird ein zusätzlicher Opcode hinzugefügt. Und im Fall von isset () wird nur ein eindeutiger Opcode verwendet.

Ein Beispiel für eine zerlegte if (strlen ()) - Struktur :

line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
   3     0  >   SEND_VAR                                                 !0
         1      DO_FCALL                                      1  $0      'strlen'
         2      IS_SMALLER                                       ~1      42, $0
         3    > JMPZ                                                     ~1, ->5
   5     4  > > JMP                                                      ->5
   6     5  > > RETURN                                                   1

Und hier ist die semantisch äquivalente Struktur von if (isset ()) :

line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
   3     0  >   ISSET_ISEMPTY_DIM_OBJ                       33554432  ~0      !0, 42
         1    > JMPZ                                                  ~0, ->3
   5     2  > > JMP                                                       ->3
   6     3  > > RETURN                                                     1

Wie Sie sehen können, beinhaltet der isset () - Code keinen Aufruf einer Funktion (DO_FCALL). Außerdem gibt es keinen Opcode IS_SMALLER (ignorieren Sie einfach die RETURN-Anweisungen); isset () gibt direkt einen booleschen Wert zurück; strlen () gibt zuerst eine temporäre Variable zurück, dann wird sie an den Opcode IS_SMALLER übergeben und das Endergebnis wird mit if () berechnet . Das heißt , zwei Opcodes werden in der strlen () - Struktur verwendet , und einer wird in der isset () - Struktur verwendet . Daher zeigt isset () eine bessere Leistung, da eine Operation normalerweise schneller als zwei ist.

Lassen Sie uns nun sehen, wie Funktionsaufrufe in PHP funktionieren und wie sie sich von unterscheidenisset () .

PHP-Funktionsaufrufe


Am schwierigsten ist es, den Teil der virtuellen Maschine zu analysieren (der Moment, in dem der PHP-Code ausgeführt wird), der Funktionsaufrufen zugeordnet ist. Ich werde versuchen, das Wesentliche darzulegen, ohne auf die Momente einzugehen, die mit Funktionsaufrufen zusammenhängen.

Lassen Sie uns zunächst die Laufzeit von Anrufen analysieren . Zur Kompilierungszeit sind viele Ressourcen erforderlich, um Vorgänge im Zusammenhang mit PHP-Funktionen auszuführen. Wenn Sie jedoch den Opcode-Cache verwenden, treten beim Kompilieren keine Probleme auf.

Angenommen, wir haben ein bestimmtes Skript kompiliert. Lassen Sie uns nur analysieren, was zur Laufzeit passiert . So sieht der Speicherauszug des Opcode-Aufrufs für die interne Funktion aus (in diesem Fall sieht strlen () aus ):

strlen($a);
line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
3     0  >   SEND_VAR                                                 !0
      1      DO_FCALL                                      1          'strlen'

Um den Funktionsaufrufmechanismus zu verstehen, müssen Sie zwei Dinge wissen:

  • Funktionsaufruf und Methodenaufruf sind dasselbe
  • Ein Benutzerfunktionsaufruf und ein interner Funktionsaufruf werden unterschiedlich behandelt

Aus diesem Grund wird im letzten Beispiel die „interne“ Funktion aufgerufen : strlen () ist eine PHP-Funktion, die Teil des C-Codes ist. Wenn wir den Opcode einer "benutzerdefinierten" PHP-Funktion (dh einer in PHP geschriebenen Funktion) ausgeben, könnten wir entweder genau den gleichen oder einen anderen Opcode erhalten.

Tatsache ist, dass unabhängig davon, ob diese Funktion PHP bekannt ist oder nicht, beim Kompilieren nicht derselbe Opcode generiert wird. Offensichtlich sind interne PHP-Funktionen zur Kompilierungszeit bekannt, da sie vor dem Start des Compilers deklariert werden. Es besteht jedoch möglicherweise keine Klarheit hinsichtlich benutzerdefinierter Funktionen, da diese möglicherweise aufgerufen werden, bevor sie deklariert werden. Wenn wir über die Ausführung sprechen, sind interne PHP-Funktionen effizienter als benutzerdefinierte. Außerdem stehen mehr Validierungsmechanismen zur Verfügung.

Aus dem obigen Beispiel ist ersichtlich, dass mehr als ein Opcode zur Steuerung von Funktionsaufrufen verwendet wird. Sie müssen sich auch daran erinnern, dass Funktionen einen eigenen Stapel haben. In PHP müssen Sie wie in jeder anderen Sprache zum Aufrufen einer Funktion zunächst einen Stapelrahmen erstellen und Argumente an die Funktion übergeben. Anschließend rufen Sie eine Funktion auf, die diese Argumente für Ihre Anforderungen aus dem Stapel zieht. Am Ende des Aufrufs müssen Sie den zuvor erstellten Frame zerstören.

So sieht das Schema der Arbeit mit Funktionsaufrufen in allgemeiner Form aus. PHP bietet jedoch eine Optimierung der Verfahren zum Erstellen und Löschen eines Stapelrahmens. Darüber hinaus können Sie die Ausführung verschieben, sodass Sie nicht alle diese Gesten bei jedem Funktionsaufruf ausführen müssen.

Der Opcode SEND_VAR ist für das Senden von Argumenten an den Stapelrahmen verantwortlich. Der Compiler generiert ohne Fehler einen solchen Opcode, bevor er die Funktion aufruft. Darüber hinaus wird für jede Variable eine eigene erstellt:

$a = '/';
setcookie('foo', 'bar', 128, $a);
line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
   3     0  >   ASSIGN                                                   !0, '%2F'
   4     1      SEND_VAL                                                 'foo'
         2      SEND_VAL                                                 'bar'
         3      SEND_VAL                                                 128
         4      SEND_VAR                                                 !0
         5      DO_FCALL                                      4          'setcookie'

Hier sehen Sie einen anderen Opcode - SEND_VAL. Insgesamt gibt es 4 Arten von Opcodes, um etwas an den Funktionsstapel zu senden:

  • SEND_VAL : Sendet einen konstanten Wert (Zeichenfolge, Ganzzahl usw.)
  • SEND_VAR : Sendet eine PHP-Variable ($ a)
  • SEND_REF : Sendet eine PHP-Variable als Link an eine Funktion, die ein Argument mit einem Link akzeptiert
  • SEND_VAR_NO_REF : Optimierter Handler, der in Fällen mit verschachtelten Funktionen verwendet wird


Was macht SEND_VAR?

ZEND_VM_HELPER(zend_send_by_var_helper, VAR|CV, ANY)
{
    USE_OPLINE
    zval *varptr;
    zend_free_op free_op1;
    varptr = GET_OP1_ZVAL_PTR(BP_VAR_R);
    if (varptr == &EG(uninitialized_zval)) {
        ALLOC_ZVAL(varptr);
        INIT_ZVAL(*varptr);
        Z_SET_REFCOUNT_P(varptr, 0);
    } else if (PZVAL_IS_REF(varptr)) {
        zval *original_var = varptr;
        ALLOC_ZVAL(varptr);
        ZVAL_COPY_VALUE(varptr, original_var);
        Z_UNSET_ISREF_P(varptr);
        Z_SET_REFCOUNT_P(varptr, 0);
        zval_copy_ctor(varptr);
    }
    Z_ADDREF_P(varptr);
    zend_vm_stack_push(varptr TSRMLS_CC);
    FREE_OP1();  /* for string offsets */
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

SEND_VAR prüft, ob die Variable eine Referenz ist. Wenn ja, wird es getrennt, wodurch eine Verbindungsfehlanpassung entsteht. Warum das so schlimm ist, können Sie in einem anderen Artikel von mir nachlesen . Dann fügt SEND_VAR der Variablen die Anzahl der Links hinzu (der Link hier ist kein Link in Bezug auf PHP, dh nicht das &, sondern nur ein Indikator dafür, wie viele diesen Wert verwenden) und sendet ihn an den Stapel der virtuellen Maschine:

Z_ADDREF_P(varptr);
zend_vm_stack_push(varptr TSRMLS_CC);

Jedes Mal, wenn Sie eine Funktion aufrufen, erhöhen Sie den Refcount jedes Variablenarguments zum Stapel um eins. Dies liegt daran, dass auf die Variable nicht durch den Funktionscode, sondern durch ihren Stapel verwiesen wird. Das Senden einer Variablen an den Stapel hat nur geringe Auswirkungen auf die Leistung, der Stapel beansprucht jedoch Speicher. Es wird zur Laufzeit darin abgelegt, aber seine Größe wird zur Kompilierungszeit berechnet. Nachdem Sie die Variable an den Stack gesendet haben, führen Sie DO_FCALL aus. Im Folgenden finden Sie ein Beispiel dafür, wie viel Code und Überprüfungen nur verwendet werden, damit wir Aufrufe von PHP-Funktionen als „langsame Anweisungen“ betrachten:

ZEND_VM_HANDLER(60, ZEND_DO_FCALL, CONST, ANY)
{
    USE_OPLINE
    zend_free_op free_op1;
    zval *fname = GET_OP1_ZVAL_PTR(BP_VAR_R);
    call_slot *call = EX(call_slots) + opline->op2.num;
    if (CACHED_PTR(opline->op1.literal->cache_slot)) {
        EX(function_state).function = CACHED_PTR(opline->op1.literal->cache_slot);
    } else if (UNEXPECTED(zend_hash_quick_find(EG(function_table), Z_STRVAL_P(fname), Z_STRLEN_P(fname)+1, Z_HASH_P(fname), (void **) &EX(function_state).function)==FAILURE)) {
        SAVE_OPLINE();
        zend_error_noreturn(E_ERROR, "Call to undefined function %s()", fname->value.str.val);
    } else {
        CACHE_PTR(opline->op1.literal->cache_slot, EX(function_state).function);
    }
    call->fbc = EX(function_state).function;
    call->object = NULL;
    call->called_scope = NULL;
    call->is_ctor_call = 0;
    EX(call) = call;
    FREE_OP1();
    ZEND_VM_DISPATCH_TO_HELPER(zend_do_fcall_common_helper);
}

Wie Sie sehen können, wurden hier kleine Überprüfungen durchgeführt und verschiedene Caches verwendet. Beispielsweise hat der Handlerzeiger den allerersten Aufruf gefunden und wurde dann im Hauptrahmen der virtuellen Maschine zwischengespeichert, damit jeder nachfolgende Aufruf diesen Zeiger verwenden kann.

Als nächstes rufen wir zend_do_fcall_common_helper () auf . Ich werde den Code dieser Funktion hier nicht posten, er ist zu umfangreich. Ich werde nur die Operationen zeigen, die dort durchgeführt wurden. Kurz gesagt, dies sind viele verschiedene Überprüfungen, die während der Ausführung durchgeführt werden. PHP ist eine dynamische Sprache, die zur Laufzeit neue Funktionen und Klassen deklarieren und gleichzeitig automatisch Dateien hochladen kann. Daher ist PHP gezwungen, zur Laufzeit viele Überprüfungen durchzuführen, was sich nachteilig auf die Leistung auswirkt. Aber daran führt kein Weg vorbei.

if (UNEXPECTED((fbc->common.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_DEPRECATED)) != 0)) {
        if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_ABSTRACT) != 0)) {
            zend_error_noreturn(E_ERROR, "Cannot call abstract method %s::%s()", fbc->common.scope->name, fbc->common.function_name);
            CHECK_EXCEPTION();
            ZEND_VM_NEXT_OPCODE(); /* Never reached */
        }
        if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) {
            zend_error(E_DEPRECATED, "Function %s%s%s() is deprecated",
                fbc->common.scope ? fbc->common.scope->name : "",
                fbc->common.scope ? "::" : "",
                fbc->common.function_name);
        }
    }
    if (fbc->common.scope &&
        !(fbc->common.fn_flags & ZEND_ACC_STATIC) &&
        !EX(object)) {
        if (fbc->common.fn_flags & ZEND_ACC_ALLOW_STATIC) {
            /* FIXME: output identifiers properly */
            zend_error(E_STRICT, "Non-static method %s::%s() should not be called statically", fbc->common.scope->name, fbc->common.function_name);
        } else {
            /* FIXME: output identifiers properly */
            /* An internal function assumes $this is present and won't check that. So PHP would crash by allowing the call. */
            zend_error_noreturn(E_ERROR, "Non-static method %s::%s() cannot be called statically", fbc->common.scope->name, fbc->common.function_name);
        }
    }

Sehen Sie, wie viele Schecks? Gehen Sie voran:

if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) {
    should_change_scope = 1;
    EX(current_this) = EG(This);
    EX(current_scope) = EG(scope);
    EX(current_called_scope) = EG(called_scope);
    EG(This) = EX(object);
    EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope : NULL;
    EG(called_scope) = EX(call)->called_scope;
}

Sie wissen, dass jeder Funktionskörper seinen eigenen Bereich für eine Variable hat. Die Engine wechselt die Sichtbarkeitstabellen, bevor sie den Funktionscode aufruft. Wenn sie also eine Variable anfordert, wird diese in der entsprechenden Tabelle gefunden. Und da Funktionen und Methoden im Wesentlichen dasselbe sind, können Sie lesen, wie Sie den Zeiger $ this an eine Methode binden .

if (fbc->type == ZEND_INTERNAL_FUNCTION) {

Wie oben erwähnt, haben interne Funktionen (die Teil von C sind) einen anderen Ausführungspfad, der nicht mit benutzerdefinierten Funktionen identisch ist. Normalerweise ist es kürzer und besser optimiert, da wir dem Motor Informationen über interne Funktionen mitteilen können, die nicht über benutzerdefinierte Funktionen gesagt werden können.

fbc->internal_function.handler(opline->extended_value, ret->var.ptr, (fbc->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) ? &ret->var.ptr : NULL, EX(object), RETURN_VALUE_USED(opline) TSRMLS_CC);

Die obige Zeile ruft den internen Funktionshandler auf. In unserem Beispiel zu strlen () ruft diese Zeile den Code auf:

/* PHP's strlen() source code */
ZEND_FUNCTION(strlen)
{
    char *s1;
    int s1_len;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s1, &s1_len) == FAILURE) {
        return;
    }
    RETVAL_LONG(s1_len);
}

Was macht strlen () ? Es ruft ein Argument mit zend_parse_parameters () vom Stapel ab . Dies ist eine "langsame" Funktion, da sie den Stapel anheben und das Argument in den von der Funktion erwarteten Typ konvertieren muss (in unserem Fall eine Zeichenfolge). Unabhängig davon, was Sie für strlen () an den Stack übergeben , muss das Argument möglicherweise konvertiert werden. Dies ist in Bezug auf die Leistung nicht der einfachste Prozess. Der Quellcode zend_parse_parameters () gibt eine gute Vorstellung davon, wie viele Operationen der Prozessor ausführen muss, während er die Argumente aus dem Stapelrahmen der Funktion abruft .

Fahren Sie mit dem nächsten Schritt fort. Wir haben gerade den Funktionskörpercode ausgeführt, jetzt müssen wir „aufräumen“. Beginnen wir mit der Wiederherstellung des Gültigkeitsbereichs:

if (should_change_scope) {
        if (EG(This)) {
            if (UNEXPECTED(EG(exception) != NULL) && EX(call)->is_ctor_call) {
                if (EX(call)->is_ctor_result_used) {
                    Z_DELREF_P(EG(This));
                }
                if (Z_REFCOUNT_P(EG(This)) == 1) {
                    zend_object_store_ctor_failed(EG(This) TSRMLS_CC);
                }
            }
            zval_ptr_dtor(&EG(This));
        }
        EG(This) = EX(current_this);
        EG(scope) = EX(current_scope);
        EG(called_scope) = EX(current_called_scope);
    }

Dann löschen Sie den Stapel:

zend_vm_stack_clear_multiple(1 TSRMLS_CC);

Und wenn während der Ausführung der Funktion einige Ausnahmen gemacht wurden, müssen Sie die virtuelle Maschine zum catch-Block dieser Ausnahme leiten:

if (UNEXPECTED(EG(exception) != NULL)) {
        zend_throw_exception_internal(NULL TSRMLS_CC);
        if (RETURN_VALUE_USED(opline) && EX_T(opline->result.var).var.ptr) {
            zval_ptr_dtor(&EX_T(opline->result.var).var.ptr);
        }
        HANDLE_EXCEPTION();
    }

Über PHP-Funktionsaufrufe


Jetzt können Sie sich vorstellen, wie viel Zeit der Computer damit verbringt, die Funktion "sehr klein und einfach" strlen () aufzurufen . Und da es mehrfach aufgerufen wird, erhöhen Sie diese Zeit beispielsweise um das 25.000-fache. So werden Mikro- und Millisekunden zu vollen Sekunden ... Bitte beachten Sie, dass ich bei jedem Aufruf der PHP-Funktion nur die wichtigsten Anweisungen für uns demonstriert habe. Danach passieren viel interessantere Dinge. Beachten Sie auch, dass im Fall von strlen () nur eine Zeile „nützliche Arbeit“ ausführt und die zugehörigen Verfahren zum Vorbereiten eines Funktionsaufrufs in Volumen größer sind als der „nützliche“ Teil des Codes. In den meisten Fällen wirkt sich der native Funktionscode jedoch immer noch stärker auf die Leistung aus als der "zusätzliche" Engine-Code.

Der Teil des PHP-Codes, der sich auf Funktionsaufrufe in PHP 7 bezieht, wurde neu gestaltet, um die Leistung zu verbessern. Dies ist jedoch noch lange nicht alles und der PHP-Quellcode wird mit jeder neuen Version mehr als einmal optimiert. Ältere Versionen wurden nicht vergessen, Funktionsaufrufe wurden in Versionen von 5.3 bis 5.5 optimiert. In Versionen von 5.4 bis 5.5 wurde beispielsweise die Methode zum Berechnen und Erstellen eines Stapelrahmens (unter Wahrung der Kompatibilität) geändert. Aus Gründen des Interesses können Sie die Änderungen im Laufzeitmodul und die Methode zum Aufrufen von Funktionen in Version 5.5 im Vergleich zu 5.4 vergleichen.

Ich möchte betonen: All dies bedeutet nicht, dass PHP schlecht ist. Diese Sprache entwickelt sich seit 20 Jahren, viele sehr talentierte Programmierer haben an ihrem Quellcode gearbeitet. In diesem Zeitraum wurde es viele Male verarbeitet, optimiert und verbessert. Ein Beweis dafür ist die Tatsache, dass Sie heute PHP verwenden und es zeigt eine gute Gesamtleistung in einer Vielzahl von Projekten.

Was ist mit isset ()?


Dies ist keine Funktion, Klammern bedeuten nicht unbedingt „Funktionsaufruf“. isset () ist im speziellen Opcode der virtuellen Zend-Maschine (ISSET_ISEMPTY) enthalten, die keinen Funktionsaufruf initiiert und nicht den damit verbundenen Verzögerungen unterliegt. Da isset () verschiedene Arten von Parametern verwenden kann, ist der Code in der virtuellen Maschine von Zend ziemlich lang. Wenn Sie jedoch nur den Teil belassen, der sich auf den Offset-Parameter bezieht, erhalten Sie ungefähr Folgendes:

ZEND_VM_HELPER_EX(zend_isset_isempty_dim_prop_obj_handler, VAR|UNUSED|CV, CONST|TMP|VAR|CV, int prop_dim)
{
    USE_OPLINE zend_free_op free_op1, free_op2; zval *container; zval **value = NULL; int result = 0; ulong hval; zval *offset;
    SAVE_OPLINE();
    container = GET_OP1_OBJ_ZVAL_PTR(BP_VAR_IS);
    offset = GET_OP2_ZVAL_PTR(BP_VAR_R);
    /* ... code pruned ... */
    } else if (Z_TYPE_P(container) == IS_STRING && !prop_dim) { /* string offsets */
        zval tmp;
        /* ... code pruned ... */
        if (Z_TYPE_P(offset) == IS_LONG) { /* we passed an integer as offset */
            if (opline->extended_value & ZEND_ISSET) {
                if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container)) {
                    result = 1;
                }
            } else /* if (opline->extended_value & ZEND_ISEMPTY) */ {
                if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container) && Z_STRVAL_P(container)[offset->value.lval] != '0') {
                    result = 1;
                }
            }
        }
        FREE_OP2();
    } else {
        FREE_OP2();
    }
    Z_TYPE(EX_T(opline->result.var).tmp_var) = IS_BOOL;
    if (opline->extended_value & ZEND_ISSET) {
        Z_LVAL(EX_T(opline->result.var).tmp_var) = result;
    } else {
        Z_LVAL(EX_T(opline->result.var).tmp_var) = !result;
    }
    FREE_OP1_VAR_PTR();
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

Wenn Sie die zahlreichen Entscheidungspunkte ( wenn Konstrukte ) entfernen , kann der "Haupt" -Berechnungsalgorithmus als Linie ausgedrückt werden:

if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container))

Wenn der Offset größer als Null ist (Sie meinten nicht isset ($ a [-42]) ) und streng kleiner als die Länge der Zeichenfolge ist, wird das Ergebnis als 1 angenommen. Dann ist das Ergebnis der Operation Boolean TRUE. Machen Sie sich keine Sorgen um die Berechnung der Länge, Z_STRLEN_P (Container) berechnet nichts. Denken Sie daran, dass PHP die Länge Ihrer Zeichenfolge bereits kennt. Z_STRLEN_P (Container) liest diesen Wert einfach in den Speicher, der extrem wenig Prozessorressourcen verbraucht.

Jetzt verstehen Sie, warum die Verarbeitung von strlen () -Aufrufen im Hinblick auf die Verwendung des Zeilenversatzes VIEL mehr Rechenressourcen erfordert als isset (). Letzteres ist wesentlich "einfacher". Haben Sie keine Angst vor einer großen Anzahl von bedingten if-Anweisungen, dies ist nicht der schwierigste Teil des C-Codes. Darüber hinaus können sie mit dem C-Compiler optimiert werden. Der Handlercode isset () sucht nicht in Hash-Tabellen, führt keine komplexen Überprüfungen durch und weist keinem der Stapelrahmen einen Zeiger zu, um ihn später abzurufen . Der Code ist viel leichter als der allgemeine Funktionsaufrufcode und greift viel seltener auf den Speicher zu (dies ist der wichtigste Punkt). Und wenn Sie die Mehrfachausführung einer solchen Zeile wiederholen, können Sie eine große Leistungsverbesserung erzielen. Natürlich unterscheiden sich die Ergebnisse einer Iteration von strlen () und isset () nicht wesentlich - um etwa 5 ms. Aber wenn Sie 50.000 Iterationen ausgeben ...

Beachten Sie auch, dass isset ()und empty () sind fast der gleiche Quellcode . Wenn versetzten Reihen leer () wird von dem abweichen , isset () nur zusätzliche Lesen, wenn das erste Zeichen nicht 0 ist Da die Codes sind leer () und isset () unterscheiden sich nicht voneinander, die leer () zeigt die gleiche Leistung als isset () ( vorausgesetzt , beide werden mit denselben Parametern verwendet).

Wie OPCache uns helfen kann


Kurz gesagt, nichts.

OPCache optimiert den Code. Sie können darüber in der Präsentation lesen . Oft wird gefragt, ob es möglich ist, einen Optimierungsdurchlauf hinzuzufügen, in dem strlen () zu isset () wechselt . Nein, das ist nicht möglich.

OPCache-Optimierungsdurchläufe werden in OPArray implementiert, bevor sie im gemeinsam genutzten Speicher abgelegt werden. Dies geschieht zur Kompilierungszeit, nicht zur Laufzeit. Woher wissen wir zur Kompilierungszeit, dass die an strlen () übergebene Variable eine Zeichenfolge ist ?? Dies ist ein bekanntes PHP-Problem, das teilweise mit HHVM / Hack gelöst wird. Wenn wir unsere Variablen mit strikter Typisierung in PHP schreiben, können während der Compiler-Durchläufe viel mehr Dinge optimiert werden (wie in einer virtuellen Maschine). Da PHP eine dynamische Sprache ist, ist zur Kompilierungszeit fast nichts bekannt. OPCache kann nur statische Dinge optimieren, die zum Zeitpunkt des Kompilierungsstarts bekannt waren. Zum Beispiel dieses:

if (strlen("foo") > 8) {
/* do domething */
} else {
/* do something else */
}

Zur Kompilierungszeit ist bekannt, dass die Länge der Zeichenfolge "foo" nicht mehr als 8 beträgt. Sie können also den gesamten Opcode if () wegwerfen und nur den else-Teil aus dem if-Konstrukt belassen.

if (strlen($a) > 8) {
/* do domething */
} else {
/* do something else */
}

Aber was ist $ a ? Existiert es überhaupt? Ist das eine Zeichenfolge? Zum Zeitpunkt des Bestehens des Optimierers können wir noch nicht alle diese Fragen beantworten - dies ist die Aufgabe für das ausführende Modul der virtuellen Maschine. Zur Kompilierungszeit verarbeiten wir die abstrakte Struktur, und die Art und Menge des benötigten Speichers sind zur Laufzeit bekannt.

OPCache optimiert viele Dinge, aber aufgrund der Natur von PHP kann es nicht alles optimieren. Zumindest nicht so viel wie im Java- oder C.-Compiler. Leider wird PHP niemals eine stark typisierte Sprache sein. Es gibt auch gelegentliche Vorschläge zum Einfügen von schreibgeschützten Hinweisen in die Deklaration von Klasseneigenschaften:

class Foo {
    public read-only $a = "foo";
}

Wenn Sie die Funktionalität nicht berühren, sind solche Vorschläge im Hinblick auf die Leistungsoptimierung sinnvoll. Wenn wir eine ähnliche Klasse kompilieren, kennen wir den Wert von $ a . Wir wissen, dass es sich nicht ändern wird. Sie können es also irgendwo speichern, einen zwischengespeicherten Zeiger verwenden und jede Iteration des Zugriffs auf eine ähnliche Variable optimieren, wenn Sie den Compiler oder OPCache übergeben. Die Hauptidee dabei ist, dass je mehr Informationen Sie dem Compiler über den Typ und die Verwendung Ihrer Variablen oder Funktionen geben können, desto mehr OPCache optimieren kann, desto näher kommt das Ergebnis den Anforderungen des Prozessors.

Optimierungstipps und abschließende Worte


Zunächst empfehle ich, Ihren Code nicht gedankenlos an den Stellen zu ändern, an denen er Ihnen wirklich gut erscheint. Profil und sehen Sie die Ergebnisse. Dank Profilern wie Blackfire können Sie den Prozess der Ausführung Ihres Skripts sofort sehen, da alle irrelevanten Informationen, die die Essenz beeinträchtigen, automatisch verworfen werden. Sie werden verstehen, wo Sie mit der Arbeit beginnen müssen. Schließlich kostet Ihre Arbeit Geld und muss auch optimiert werden. Dies ist eine gute Balance zwischen dem Geld, das Sie für die Optimierung des Skripts ausgeben, und dem Betrag, den Sie sparen, indem Sie die Kosten für die Wartung der Cloud senken.

Zweiter Tipp: PHP ist sehr schnell, effizient und zuverlässig. Es gibt nicht viele Möglichkeiten, PHP-Skripte zu optimieren - zum Beispiel sind sie weniger als in untergeordneten Sprachen wie C. Daher sollten Optimierungsbemühungen auf Schleifen ohne bestimmte Bedingungen zum Beenden dieser Skripte gerichtet werden. Wenn der Profiler einen Skriptengpass anzeigt, befindet er sich höchstwahrscheinlich innerhalb der Schleife. Hier häufen sich winzige Verzögerungen in vollen Sekunden an, da die Anzahl der Iterationen in Zyklen in Zehntausenden gemessen wird. In PHP sind solche Schleifen bis auf foreach () identisch und führen zu demselben Opcode. Ändern Sie sie , während auf für sinnlos, und der Profiler Sie werden es beweisen.

Bei Funktionsaufrufen gibt es kleine Tricks, um zu verhindern, dass einige Aufrufe getätigt werden, da die Informationen an anderer Stelle verfügbar sind.

phpversion() => use the PHP_VERSION constant
php_uname() => use the PHP_OS constant
php_sapi_name() => use the PHP_SAPI constant
time() => read $_SERVER['REQUEST_TIME']
session_id() => use the SID constant

Die angegebenen Beispiele sind nicht immer vollständig gleichwertig. Einzelheiten finden Sie in der Dokumentation.

Ja, und versuchen Sie, solchen Unsinn zu vermeiden wie:

function foo() {
    bar();
}

Oder schlimmer:

function foo() {
    call_user_func_array('bar', func_get_args());
}

Gehen Sie bei der Optimierung immer kritisch vor, tun Sie dies nicht nur, weil Sie irgendwo etwas gehört haben oder jemand Sie beraten hat. Unterordnen Sie die Struktur Ihrer Anwendung nicht dem Erreichen maximaler Leistung, sondern arbeiten Sie im Rahmen der individuellen Fähigkeiten Ihrer Idee daran.

Profilieren Sie häufig Ihr Skript und überprüfen Sie jede Annahme. Befolgen Sie nicht blind die Anweisungen anderer. Überprüfen Sie immer alles.

Die Entwickler des Blackfire-Profilers haben in ihr Produkt einen Mechanismus zum Sammeln interessanter Metriken integriert, die den Benutzern bei ihrer Arbeit helfen können. Viele Parameter werden protokolliert (obwohl die GUI noch nicht alles anzeigt): Wenn beispielsweise der Garbage Collector gestartet wird, was er tut, wie viele Objekte in Funktionen erstellt / zerstört werden, wie viele Linkinkonsistenzen während Funktionsaufrufen erstellt wurden, Serialisierungszeit, falsches foreach () - Verhalten und usw. usw.

Vergessen Sie auch nicht, dass Sie eines Tages auf Sprachbeschränkungen stoßen werden. Vielleicht ist es dann ratsam, eine andere zu wählen. PHP eignet sich nicht zum Erstellen von ORMs, Videospielen, HTTP-Servern und vielen anderen Aufgaben. Es kann dafür verwendet werden, aber es wird ineffizient sein. Daher ist es für jede Aufgabe besser, die am besten geeignete Sprache auszuwählen, da heute eine große Auswahl besteht: Java, Go oder wahrscheinlich die effektivste Sprache unserer Zeit - C / C ++ (Java und Go sind speziell darauf geschrieben).

Jetzt auch beliebt: