From 8e216a2f3c4c0c97ffa5aecaafd3f14d1911302a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 8 Apr 2025 10:46:26 +0200 Subject: [PATCH 1/2] zend_execute: Improve type checking performance when passing a `Closure` to a `callable` parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `Closure`s existing since 5.3 and first class callables since 8.1, nowadays the most common type of callable likely is a `Closure`. Type checking a `Closure` argument for a `callable` parameter however does quite a bit of work, when a simple CE check would suffice. We thus do this. For: Date: Tue, 8 Apr 2025 14:04:31 +0200 Subject: [PATCH 2/2] zend_execute: Streamline typechecks in `zend_check_type_slow()` if an object is given MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the callable.php vs closure.php benchmark: $ taskset -c 0 hyperfine -L file callable.php,closure.php -L version before,after '/tmp/bench/{version} {file}' Benchmark 1: /tmp/bench/before callable.php Time (mean ± σ): 351.1 ms ± 2.8 ms [User: 348.3 ms, System: 2.0 ms] Range (min … max): 349.3 ms … 359.0 ms 10 runs Benchmark 2: /tmp/bench/before closure.php Time (mean ± σ): 274.6 ms ± 2.4 ms [User: 270.9 ms, System: 2.9 ms] Range (min … max): 273.3 ms … 281.5 ms 10 runs Benchmark 3: /tmp/bench/after callable.php Time (mean ± σ): 272.4 ms ± 0.5 ms [User: 270.3 ms, System: 2.0 ms] Range (min … max): 271.6 ms … 273.3 ms 10 runs Benchmark 4: /tmp/bench/after closure.php Time (mean ± σ): 277.4 ms ± 2.2 ms [User: 274.3 ms, System: 2.4 ms] Range (min … max): 275.7 ms … 283.3 ms 10 runs Summary /tmp/bench/after callable.php ran 1.01 ± 0.01 times faster than /tmp/bench/before closure.php 1.02 ± 0.01 times faster than /tmp/bench/after closure.php 1.29 ± 0.01 times faster than /tmp/bench/before callable.php For the array_find benchmark: Benchmark 1: /tmp/bench/before native.php Time (mean ± σ): 627.6 ms ± 7.1 ms [User: 622.5 ms, System: 2.8 ms] Range (min … max): 622.1 ms … 641.4 ms 10 runs Benchmark 2: /tmp/bench/after native.php Time (mean ± σ): 598.0 ms ± 5.5 ms [User: 594.4 ms, System: 2.7 ms] Range (min … max): 589.9 ms … 604.9 ms 10 runs Summary /tmp/bench/after native.php ran 1.05 ± 0.02 times faster than /tmp/bench/before native.php For a test with scalar types: get()); } for ($i = 0; $i < 10000000; $i++) { func(new A()); } results in: Benchmark 1: /tmp/bench/before union_obj.php Time (mean ± σ): 700.6 ms ± 10.4 ms [User: 696.5 ms, System: 3.9 ms] Range (min … max): 688.6 ms … 717.8 ms 10 runs Benchmark 2: /tmp/bench/after union_obj.php Time (mean ± σ): 706.7 ms ± 15.0 ms [User: 702.5 ms, System: 3.8 ms] Range (min … max): 688.9 ms … 725.8 ms 10 runs Summary /tmp/bench/before union_obj.php ran 1.01 ± 0.03 times faster than /tmp/bench/after union_obj.php --- Zend/zend_execute.c | 48 +++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 69dc2d2353140..ae50771c6357f 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1088,9 +1088,7 @@ static zend_never_inline zval* zend_assign_to_typed_prop(const zend_property_inf } static zend_always_inline bool zend_value_instanceof_static(const zval *zv) { - if (Z_TYPE_P(zv) != IS_OBJECT) { - return 0; - } + ZEND_ASSERT(Z_TYPE_P(zv) == IS_OBJECT); zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data)); if (!called_scope) { @@ -1144,9 +1142,24 @@ static zend_always_inline bool zend_check_type_slow( const zend_type *type, zval *arg, const zend_reference *ref, bool is_return_type, bool is_internal) { - if (ZEND_TYPE_IS_COMPLEX(*type) && EXPECTED(Z_TYPE_P(arg) == IS_OBJECT)) { - zend_class_entry *ce; - if (UNEXPECTED(ZEND_TYPE_HAS_LIST(*type))) { + const uint32_t type_mask = ZEND_TYPE_FULL_MASK(*type); + + if (EXPECTED(Z_TYPE_P(arg) == IS_OBJECT)) { + if (EXPECTED(ZEND_TYPE_HAS_NAME(*type))) { + zend_class_entry *ce = zend_fetch_ce_from_type(type); + /* If we have a CE we check if it satisfies the type constraint, + * otherwise it will check if a standard type satisfies it. */ + if (ce && instanceof_function(Z_OBJCE_P(arg), ce)) { + return true; + } + } + if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) { + return true; + } + if ((type_mask & MAY_BE_CALLABLE) && EXPECTED(Z_OBJCE_P(arg) == zend_ce_closure)) { + return true; + } + if (EXPECTED(ZEND_TYPE_HAS_LIST(*type))) { if (ZEND_TYPE_IS_INTERSECTION(*type)) { return zend_check_intersection_type_from_list(ZEND_TYPE_LIST(*type), Z_OBJCE_P(arg)); } else { @@ -1158,7 +1171,7 @@ static zend_always_inline bool zend_check_type_slow( } } else { ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); - ce = zend_fetch_ce_from_type(list_type); + zend_class_entry *ce = zend_fetch_ce_from_type(list_type); /* Instance of a single type part of a union is sufficient to pass the type check */ if (ce && instanceof_function(Z_OBJCE_P(arg), ce)) { return true; @@ -1166,28 +1179,11 @@ static zend_always_inline bool zend_check_type_slow( } } ZEND_TYPE_LIST_FOREACH_END(); } - } else { - ce = zend_fetch_ce_from_type(type); - /* If we have a CE we check if it satisfies the type constraint, - * otherwise it will check if a standard type satisfies it. */ - if (ce && instanceof_function(Z_OBJCE_P(arg), ce)) { - return true; - } } } - const uint32_t type_mask = ZEND_TYPE_FULL_MASK(*type); - if ( - (type_mask & MAY_BE_CALLABLE) - && ( - /* Fast check for closures. */ - EXPECTED(Z_OBJCE_P(arg) == zend_ce_closure) - || zend_is_callable(arg, is_internal ? IS_CALLABLE_SUPPRESS_DEPRECATIONS : 0, NULL) - ) - ) { - return 1; - } - if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) { + if ((type_mask & MAY_BE_CALLABLE) && + zend_is_callable(arg, is_internal ? IS_CALLABLE_SUPPRESS_DEPRECATIONS : 0, NULL)) { return 1; } if (ref && ZEND_REF_HAS_TYPE_SOURCES(ref)) {