Начать, наверное, следует с того, что анонимная функция(замыкание) в PHP — это не функция, а объект класса Closure. Собственно, на этом статью можно было бы и закончить, но если кому-то интересны подробности — добро пожаловать под кат.
Дабы не быть голословным:
$func = function (){};
var_dump($func);
---------
object(Closure)#1 (0) {
}
Забегая вперёд, скажу, что на самом деле это не совсем обычный объект. Давайте разберёмся.
Например, такой код
$func = function (){
echo 'Hello world!';
};
$func();
компилируется в такой набор опкодов:
line #* E I O 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 #* E I O op fetch ext return operands
--------------------------------------------------------------------------
9 0 E > ECHO 'Hello+world%21'
10 1 > RETURN null
Блок с описанием тела функции нам не особо интересен, а вот в первом блоке присутствуют два интересных нам опкода: DECLARE_LAMBDA_FUNCTION и INIT_DYNAMIC_CALL. Начнём со второго.
INIT_DYNAMIC_CALL
Этот опкод используется в случае, когда компилятор видит вызов функции на переменной или массиве. Т.е.
$variable();
['ClassName', 'staticMethod']();
Это не какой-то уникальный опкод, специфичный только для замыканий. Такой синтаксис также работает для объектов, вызывая метод __invoke(), для строковых переменных, содержащих имя функции ($a = 'funcName'; $a();), и для массивов, содержащих имена класса и статического метода в нём.
В случае замыкания нас интересует вызов на переменной с объектом, что логично.
Углубляясь в код VM, обрабатывающий этот опкод, мы дойдём до функции zend_init_dynamic_call_object, в которой увидим следующее (нарезка):
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;
}
...
}
Забавно, что привычный всем вызов метода __invoke в терминах VM является попыткой вызова замыкания — get_closure.
Собственно, на этом месте начинается разница в обработке вызова анонимной функции и метода __invoke обычного объекта.
В PHP у каждого объекта существует набор различных обработчиков, определяющий его служебные и магические методы.
Стандартный набор выглядит так
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 */
};
Сейчас нас интересует обработчик get_closure. Для обычного объекта он указывает на функцию zend_std_get_closure, которая проверяет, что для объекта определена функция __invoke, и возвращает либо указатель на неё, либо ошибку. А вот для класса Closure, реализующего анонимные функции, в этом массиве обработчиков переопределены практически все служебные функции, включая те, которые управляют жизненным циклом. Т.е. хоть для пользователя он и выглядит как обычный объект, но на самом деле это мутант с суперспособностями :)
Регистрация обработчиков для объекта класса 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;
}
В руководстве говорится:
Кроме методов, описанных здесь, этот класс также имеет метод __invoke. Данный метод необходим только для совместимости с другими классами, в которых реализован магический вызов, так как этот метод не используется при вызове функции.
И это таки правда. Функция get_closure для замыкания возвращает не __invoke, а вашу функцию, из которой создавалось замыкание.
Более подробно можете изучить исходники сами — файл zend_closure.c, а мы перейдём к следующему опкоду.
DECLARE_LAMBDA_FUNCTION
А вот это уже опкод, который заточен исключительно под замыкания и больше ни с чем не работающий. Под капотом обработчика происходят три основные операции:
- Ищется указатель на скомпилированную функцию, которая и будет сутью замыкания.
- Определяется контекст создания замыкания (другими словами, this).
- На основе двух первых пунктов создаётся объект класса Closure.
И вот на этом месте начинаются не очень приятные новости.
Так что же не так с анонимными функциями?
Создание замыкания — операция более тяжёлая, чем создание обыкновенного объекта. Мало того что вызывается стандартный механизм создания объекта, к нему ещё добавляется и некоторое количество логики, самым неприятным из которой является копирование всего массива опкодов вашей функции в тело объекта замыкания. Само по себе это не то чтобы страшно, но ровно до того момента, пока вы не начинаете его использовать «неправильно».
Чтобы понять, где именно поджидают проблемы, разберём случаи, когда происходит создание замыкания.
Замыкание создаётся заново:
а) при каждой обработке опкода DECLARE_LAMBDA_FUNCTION.
Интуитивно — ровно тот кейс, где замыкание смотрится хорошо, но на самом деле новый объект замыкания будет создаваться на каждой итерации цикла.
foreach($values as $value){
doSomeStuff($value, function($args) { closureBody });
}
б) при каждом вызове методов bind и bindTo:
Тут замыкание будет создаваться заново также на каждой итерации.
$closure = function($args) { closureBody };
foreach($objects as $object){
$closure->bindTo($object);
$object->doSomeStuff($closure);
}
с) при каждом вызове метода call, если в качестве функции используется генератор. А если не генератор, а обычная функция, то выполняется только часть с копированием массива опкодов. Такие дела.
Выводы
Если вам не важна производительность любой ценой, то анонимные функции удобны и приятны. А если важна, то, наверное, не стоит.
В любом случае теперь вы знаете, что замыкания и циклы, если их готовить неправильно, — такая себе комбинация.
Спасибо за внимание!
Комментарии (16)
skazo4nik
05.12.2019 15:02«Доктор, а откуда у вас такие картинки?»
Спасибо. Где бы еще подобного почитать? Чтоб не совсем хардкорный phpinternals, но с разбором и аналитикой.rjhdby Автор
05.12.2019 15:34У меня несколько статей есть :)
А так даже и не припомню, чтоб видел где. Собственно потому и сам взялся разбираться.
Hartmann
05.12.2019 16:53А как обстоят дела с замыканиями в функциях?
array_map(function($item){}, []);
rjhdby Автор
05.12.2019 16:55Создаётся каждый раз при исполнении этой строки (т.е. если без цикла, то один раз), а потом
array_map
протаскивает замыкание внутрь себя и начинается интрисиковая магия. Как оно там внутри используется — надо лазить по исходникам.
shushu
06.12.2019 07:22Вопрос немного не по теме: а как вы вообще опкоды генерите?
Попытался найти решение: однозначного ответа не нашел, много разных инструментов. Вы какой использовали?
vlreshet
Со стрелочными функциями в PHP 7.4 всё так же само? Или они тяжелее/легче?
rjhdby Автор
Да, стрелочные функции — это те же самые замыкания, но с некоторыми нюансами в логике.
Кусок функции
find_implicit_binds_recursively
, которая, в случае стрелочной функции, вызывается рекурсивно.Возможно есть еще какие небольшие отличия во внутренностях, но это надо копать. В любом случае, стрелочная функция обрабатывается тем же опкодом DECLARE_LAMBDA_FUNCTION(а это "тупой" опкод, без
ext
значений), что и замыкание. Печаль.vlreshet
Жаль. Я надеялся, что если для неё не создаётся собственный контекст, а используется родительский, то и накладные расходы меньше.
php7
Тут есть еще кто-то, кому кажется, что из-за стрелочных функций падает читаемость?
razielvamp
На мой взгляд, php вообще находится с противоположной стороны дороги от логики, читаемости и эффективности.
Некоторые адепты данного языка пару раз заявляли "а нафига мне 2 и "2" различать в условиях, я хрен знает какие там данные в импортируемой excel таблице".
И библиотеки, к сожалению, по похожим принципам пишутся.