PHP Anonymous-Funktionen: Belichten von Black Magic-Sitzungen



Wahrscheinlich sollte man damit beginnen, dass eine anonyme Funktion (Closure) in PHP keine Funktion ist, sondern ein Objekt der Closure- Klasse. Eigentlich hätte dieser Artikel fertiggestellt werden können, aber wenn sich jemand für die Details interessiert, sei willkommen bei cat.


Um nicht unbegründet zu sein:
$func = function (){}; var_dump($func); --------- object(Closure)#1 (0) { } 

Mit Blick auf die Zukunft werde ich sagen, dass dies kein gewöhnliches Objekt ist. Lass es uns herausfinden.

Zum Beispiel ein solcher Code
 $func = function (){ echo 'Hello world!'; }; $func(); 

kompiliert in einen solchen Satz von Opcodes:
 line #* EIO op fetch ext return operands -------------------------------------------------------------------------- 8 0 E > DECLARE_LAMBDA_FUNCTION '%00%7Bclosure%7D%2Fin%2FcrvX50x7fabda9ed09e' 10 1 ASSIGN !0, ~1 11 2 INIT_DYNAMIC_CALL !0 3 DO_FCALL 0 11 2 > RETURN 1 Function %00%7Bclosure%7D%2Fin%2FcrvX50x7fabda9ed09e: function name: {closure} line #* EIO op fetch ext return operands -------------------------------------------------------------------------- 9 0 E > ECHO 'Hello+world%21' 10 1 > RETURN null 

Der Block mit der Beschreibung des Funktionskörpers ist für uns nicht besonders interessant, aber im ersten Block gibt es zwei interessante Opcodes: DECLARE_LAMBDA_FUNCTION und INIT_DYNAMIC_CALL . Beginnen wir mit dem zweiten.

INIT_DYNAMIC_CALL


Dieser Opcode wird verwendet, wenn der Compiler einen Funktionsaufruf für eine Variable oder ein Array sieht. Das heißt
 $variable(); ['ClassName', 'staticMethod'](); 

Dies ist kein eindeutiger Opcode, der nur für Schließungen gilt. Diese Syntax funktioniert auch für Objekte durch Aufrufen der Methode __invoke () , für Zeichenfolgenvariablen mit dem Funktionsnamen ( $ a = 'funcName'; $ a (); ) und für Arrays mit dem Klassennamen und der darin enthaltenen statischen Methode.

Im Falle eines Abschlusses sind wir daran interessiert, eine Variable mit einem Objekt aufzurufen, was logisch ist.
Wenn wir uns näher mit dem VM-Code befassen , der diesen Opcode verarbeitet, gelangen wir zur Funktion zend_init_dynamic_call_object , in der wir Folgendes sehen werden (Slicing):
 zend_execute_data *zend_init_dynamic_call_object(zend_object *function, uint32_t num_args) { zend_function *fbc; zend_class_entry *called_scope; zend_object *object; ... if (EXPECTED(function->handlers->get_closure) && EXPECTED(function->handlers->get_closure(function, &called_scope, &fbc, &object) == SUCCESS)) { ... } else { zend_throw_error(NULL, "Function name must be a string"); return NULL; } ... } 

Es ist lustig, dass der bekannte Aufruf der Methode __invoke in Bezug auf VM der Versuch ist, closure - get_closure aufzurufen .

An diesem Punkt beginnt der Unterschied in der Verarbeitung des Aufrufs der anonymen Funktion und der Methode __invoke eines regulären Objekts.
In PHP verfügt jedes Objekt über eine Reihe unterschiedlicher Handler, mit denen das Dienstprogramm und die magischen Methoden definiert werden.
Das Standard-Set sieht so aus
 ZEND_API const zend_object_handlers std_object_handlers = { 0, /* offset */ zend_object_std_dtor, /* free_obj */ zend_objects_destroy_object, /* dtor_obj */ zend_objects_clone_obj, /* clone_obj */ zend_std_read_property, /* read_property */ zend_std_write_property, /* write_property */ zend_std_read_dimension, /* read_dimension */ zend_std_write_dimension, /* write_dimension */ zend_std_get_property_ptr_ptr, /* get_property_ptr_ptr */ NULL, /* get */ NULL, /* set */ zend_std_has_property, /* has_property */ zend_std_unset_property, /* unset_property */ zend_std_has_dimension, /* has_dimension */ zend_std_unset_dimension, /* unset_dimension */ zend_std_get_properties, /* get_properties */ zend_std_get_method, /* get_method */ zend_std_get_constructor, /* get_constructor */ zend_std_get_class_name, /* get_class_name */ zend_std_compare_objects, /* compare_objects */ zend_std_cast_object_tostring, /* cast_object */ NULL, /* count_elements */ zend_std_get_debug_info, /* get_debug_info */ /* ------- */ zend_std_get_closure, /* get_closure */ /* ------- */ zend_std_get_gc, /* get_gc */ NULL, /* do_operation */ NULL, /* compare */ NULL, /* get_properties_for */ }; 


Jetzt interessieren wir uns für den get_closure- Handler. Für ein reguläres Objekt zeigt es auf die Funktion zend_std_get_closure , die prüft, ob die Funktion __invoke für das Objekt definiert ist und entweder einen Zeiger darauf oder einen Fehler zurückgibt. Für die Closure- Klasse, die anonyme Funktionen implementiert, werden in diesem Array von Handlern jedoch fast alle Dienstprogrammfunktionen neu definiert, einschließlich derer, die den Lebenszyklus steuern. Das heißt für den Benutzer sieht es zwar aus wie ein gewöhnliches Objekt, aber in Wirklichkeit ist es eine Mutante mit Superkräften :)
Registrieren Sie Handler für ein Objekt der Klasse Closure
 void zend_register_closure_ce(void) /* {{{ */ { zend_class_entry ce; INIT_CLASS_ENTRY(ce, "Closure", closure_functions); zend_ce_closure = zend_register_internal_class(&ce); zend_ce_closure->ce_flags |= ZEND_ACC_FINAL; zend_ce_closure->create_object = zend_closure_new; zend_ce_closure->serialize = zend_class_serialize_deny; zend_ce_closure->unserialize = zend_class_unserialize_deny; memcpy(&closure_handlers, &std_object_handlers, sizeof(zend_object_handlers)); closure_handlers.free_obj = zend_closure_free_storage; closure_handlers.get_constructor = zend_closure_get_constructor; closure_handlers.get_method = zend_closure_get_method; closure_handlers.write_property = zend_closure_write_property; closure_handlers.read_property = zend_closure_read_property; closure_handlers.get_property_ptr_ptr = zend_closure_get_property_ptr_ptr; closure_handlers.has_property = zend_closure_has_property; closure_handlers.unset_property = zend_closure_unset_property; closure_handlers.compare_objects = zend_closure_compare_objects; closure_handlers.clone_obj = zend_closure_clone; closure_handlers.get_debug_info = zend_closure_get_debug_info; /* ------- */ closure_handlers.get_closure = zend_closure_get_closure; /* ------- */ closure_handlers.get_gc = zend_closure_get_gc; } 


Das Handbuch sagt:
Zusätzlich zu den hier beschriebenen Methoden verfügt diese Klasse über eine Methode __invoke . Diese Methode ist nur aus Kompatibilitätsgründen mit anderen Klassen erforderlich, in denen der Magic Call implementiert ist, da diese Methode beim Aufruf der Funktion nicht verwendet wird.

Und das ist wahr. Die Funktion get_closure für einen Abschluss gibt nicht __invoke zurück , sondern Ihre Funktion, aus der der Abschluss erstellt wurde.

Sie können die Quellen selbst genauer studieren - die Datei zend_closure.c , und wir werden zum nächsten Opcode übergehen .

DECLARE_LAMBDA_FUNCTION


Dies ist jedoch ein Opcode, der ausschließlich für die Schaltung gedacht ist und mit nichts mehr funktioniert. Unter der Haube des Prozessors gibt es drei Hauptoperationen:
  1. Es wird nach einem Zeiger auf eine kompilierte Funktion gesucht, die die Essenz des Abschlusses darstellt.
  2. Der Kontext für die Erstellung des Abschlusses (mit anderen Worten, dies ) ist definiert.
  3. Basierend auf den ersten beiden Punkten wird ein Objekt der Klasse Closure erstellt .


Und hier an diesem Ort beginnt keine sehr angenehme Nachricht.

Was stimmt also nicht mit anonymen Funktionen?


Das Erstellen eines Abschlusses ist schwieriger als das Erstellen eines normalen Objekts. Der Standardmechanismus zum Erstellen eines Objekts wird nicht nur aufgerufen, er fügt auch eine gewisse Menge an Logik hinzu, von denen die unangenehmste darin besteht, das gesamte Array der Opcodes Ihrer Funktion in den Körper des Verschlusses zu kopieren. Dies ist an sich nicht so beängstigend, aber genau so lange, bis Sie anfangen, es "falsch" zu verwenden.

Um genau zu verstehen, wo die Probleme auf Sie warten, analysieren wir die Fälle, in denen ein Abschluss erstellt wird.
Der Abschluss wird neu erstellt:
a) bei jeder Verarbeitung des DECLARE_LAMBDA_FUNCTION- Opcodes .
Intuitiv - genau in dem Fall, in dem der Verschluss gut aussieht, aber tatsächlich bei jeder Iteration der Schleife ein neues Verschlussobjekt erstellt wird.
 foreach($values as $value){ doSomeStuff($value, function($args) { closureBody }); } 

b) jedes Mal, wenn die bind- und bindTo- Methoden aufgerufen werden :
Hier wird der Verschluss auch bei jeder Iteration neu erstellt.
 $closure = function($args) { closureBody }; foreach($objects as $object){ $closure->bindTo($object); $object->doSomeStuff($closure); } 

c) bei jedem Aufruf der Aufrufmethode, wenn ein Generator als Funktion verwendet wird. Und wenn es sich nicht um einen Generator, sondern um eine gewöhnliche Funktion handelt, wird nur der Teil mit dem Kopieren des Opcode-Arrays ausgeführt. Solche Sachen.

Schlussfolgerungen


Wenn Ihnen die Leistung nicht um jeden Preis wichtig ist, sind anonyme Funktionen bequem und unterhaltsam. Und wenn wichtig, dann wahrscheinlich nicht wert.

In jedem Fall wissen Sie jetzt, dass Verschlüsse und Zyklen, wenn sie nicht richtig vorbereitet sind, eine solche Kombination sind.

Vielen Dank für Ihre Aufmerksamkeit!

Source: https://habr.com/ru/post/de478596/


All Articles