Browse Source

Add first-class callable cache

This cache is implemented in two levels: A EG(callable_convert_cache) global
that maps zend_function pointers to a shared callable instance, and a
CALLABLE_CONVERT cache slot to remember the result of the hash table lookup.

Fixes GH-19754
Closes GH-19863
pull/17433/merge
Ilija Tovilo 3 weeks ago
parent
commit
28fd7597ba
No known key found for this signature in database GPG Key ID: 115CEA7A713E12E9
  1. 4
      NEWS
  2. 6
      Zend/Optimizer/compact_literals.c
  3. 8
      Zend/tests/closures/fcc-cache.phpt
  4. 6
      Zend/tests/exit/exit_as_function.phpt
  5. 8
      Zend/tests/first_class_callable/constexpr/namespace_004.phpt
  6. 6
      Zend/tests/first_class_callable/first_class_callable_optimization.phpt
  7. 14
      Zend/tests/magic_methods/trampoline_closure_named_arguments.phpt
  8. 9
      Zend/zend_compile.c
  9. 6
      Zend/zend_execute_API.c
  10. 2
      Zend/zend_globals.h
  11. 20
      Zend/zend_vm_def.h
  12. 36
      Zend/zend_vm_execute.h
  13. 2
      Zend/zend_vm_opcodes.c
  14. 10
      ext/dom/tests/registerPhpFunctionNS.phpt

4
NEWS

@ -2,6 +2,10 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.6.0alpha1
- Core:
. Added first-class callable cache to share instances for the duration of the
request. (ilutov)
- Intl:
. Added IntlNumberRangeFormatter class to format an interval of two numbers
with a given skeleton, locale, collapse type and identity fallback.

6
Zend/Optimizer/compact_literals.c

@ -741,6 +741,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
cache_size += 2 * sizeof(void *);
}
break;
case ZEND_CALLABLE_CONVERT:
if (opline->extended_value != (uint32_t)-1) {
opline->extended_value = cache_size;
cache_size += sizeof(void *);
}
break;
}
opline++;
}

8
Zend/tests/closures/fcc-cache.phpt

@ -0,0 +1,8 @@
--TEST--
FCCs are cached and shared
--FILE--
<?php
var_dump(strlen(...) === strlen(...));
?>
--EXPECT--
bool(true)

6
Zend/tests/exit/exit_as_function.phpt

@ -19,10 +19,10 @@ foreach ($values as $value) {
}
?>
--EXPECT--
--EXPECTF--
string(4) "exit"
string(3) "die"
object(Closure)#1 (2) {
object(Closure)#%d (2) {
["function"]=>
string(4) "exit"
["parameter"]=>
@ -31,7 +31,7 @@ object(Closure)#1 (2) {
string(10) "<optional>"
}
}
object(Closure)#2 (2) {
object(Closure)#%d (2) {
["function"]=>
string(4) "exit"
["parameter"]=>

8
Zend/tests/first_class_callable/constexpr/namespace_004.phpt

@ -26,7 +26,7 @@ foo();
?>
--EXPECTF--
object(Closure)#1 (2) {
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>
@ -36,7 +36,7 @@ object(Closure)#1 (2) {
}
}
string(3) "cba"
object(Closure)#2 (2) {
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>
@ -46,7 +46,7 @@ object(Closure)#2 (2) {
}
}
string(3) "cba"
object(Closure)#2 (2) {
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>
@ -56,7 +56,7 @@ object(Closure)#2 (2) {
}
}
string(3) "cba"
object(Closure)#1 (2) {
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>

6
Zend/tests/first_class_callable/first_class_callable_optimization.phpt

@ -10,12 +10,12 @@ var_dump(test1(...));
var_dump(test2(...));
?>
--EXPECT--
object(Closure)#1 (1) {
--EXPECTF--
object(Closure)#%d (1) {
["function"]=>
string(5) "test1"
}
object(Closure)#1 (1) {
object(Closure)#%d (1) {
["function"]=>
string(5) "test2"
}

14
Zend/tests/magic_methods/trampoline_closure_named_arguments.phpt

@ -42,7 +42,7 @@ var_dump($type);
var_dump($type->getName());
?>
--EXPECT--
--EXPECTF--
-- Non-static cases --
string(4) "test"
array(3) {
@ -69,7 +69,7 @@ array(4) {
["a"]=>
int(123)
["b"]=>
object(Test)#1 (0) {
object(Test)#%d (0) {
}
}
string(4) "test"
@ -77,7 +77,7 @@ array(2) {
["a"]=>
int(123)
["b"]=>
object(Test)#1 (0) {
object(Test)#%d (0) {
}
}
string(4) "test"
@ -114,7 +114,7 @@ array(4) {
["a"]=>
int(123)
["b"]=>
object(Test)#1 (0) {
object(Test)#%d (0) {
}
}
string(10) "testStatic"
@ -122,7 +122,7 @@ array(2) {
["a"]=>
int(123)
["b"]=>
object(Test)#1 (0) {
object(Test)#%d (0) {
}
}
string(10) "testStatic"
@ -136,12 +136,12 @@ array(1) {
-- Reflection tests --
array(1) {
[0]=>
object(ReflectionParameter)#4 (1) {
object(ReflectionParameter)#%d (1) {
["name"]=>
string(9) "arguments"
}
}
bool(true)
object(ReflectionNamedType)#5 (0) {
object(ReflectionNamedType)#%d (0) {
}
string(5) "mixed"

9
Zend/zend_compile.c

@ -3965,7 +3965,14 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, zend_fun
opline->op1.num = zend_vm_calc_used_stack(0, fbc);
}
zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL);
zend_op *callable_convert_op = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL);
if (opline->opcode == ZEND_INIT_FCALL
|| opline->opcode == ZEND_INIT_FCALL_BY_NAME
|| opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME) {
callable_convert_op->extended_value = zend_alloc_cache_slot();
} else {
callable_convert_op->extended_value = (uint32_t)-1;
}
return true;
}

6
Zend/zend_execute_API.c

@ -203,6 +203,8 @@ void init_executor(void) /* {{{ */
zend_fiber_init();
zend_weakrefs_init();
zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0);
EG(active) = 1;
}
/* }}} */
@ -420,6 +422,8 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
zend_stack_clean(&EG(user_error_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1);
zend_stack_clean(&EG(user_exception_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1);
zend_hash_clean(&EG(callable_convert_cache));
#if ZEND_DEBUG
if (!CG(unclean_shutdown)) {
gc_collect_cycles();
@ -516,6 +520,8 @@ void shutdown_executor(void) /* {{{ */
if (EG(ht_iterators) != EG(ht_iterators_slots)) {
efree(EG(ht_iterators));
}
zend_hash_destroy(&EG(callable_convert_cache));
}
#if ZEND_DEBUG

2
Zend/zend_globals.h

@ -319,6 +319,8 @@ struct _zend_executor_globals {
zend_strtod_state strtod_state;
HashTable callable_convert_cache;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

20
Zend/zend_vm_def.h

@ -9709,12 +9709,28 @@ ZEND_VM_HANDLER(167, ZEND_COPY_TMP, TMPVAR, UNUSED)
ZEND_VM_NEXT_OPCODE();
}
ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED)
ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED, NUM|CACHE_SLOT)
{
USE_OPLINE
zend_execute_data *call = EX(call);
zend_closure_from_frame(EX_VAR(opline->result.var), call);
if (opline->extended_value != (uint32_t)-1) {
zend_object *closure = CACHED_PTR(opline->extended_value);
if (closure) {
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
} else {
zval *closure_zv = zend_hash_index_lookup(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func);
if (Z_TYPE_P(closure_zv) == IS_NULL) {
zend_closure_from_frame(closure_zv, call);
}
ZEND_ASSERT(Z_TYPE_P(closure_zv) == IS_OBJECT);
closure = Z_OBJ_P(closure_zv);
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
CACHE_PTR(opline->extended_value, closure);
}
} else {
zend_closure_from_frame(EX_VAR(opline->result.var), call);
}
if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) {
OBJ_RELEASE(Z_OBJ(call->This));

36
Zend/zend_vm_execute.h

@ -39087,7 +39087,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV
USE_OPLINE
zend_execute_data *call = EX(call);
zend_closure_from_frame(EX_VAR(opline->result.var), call);
if (opline->extended_value != (uint32_t)-1) {
zend_object *closure = CACHED_PTR(opline->extended_value);
if (closure) {
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
} else {
zval *closure_zv = zend_hash_index_lookup(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func);
if (Z_TYPE_P(closure_zv) == IS_NULL) {
zend_closure_from_frame(closure_zv, call);
}
ZEND_ASSERT(Z_TYPE_P(closure_zv) == IS_OBJECT);
closure = Z_OBJ_P(closure_zv);
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
CACHE_PTR(opline->extended_value, closure);
}
} else {
zend_closure_from_frame(EX_VAR(opline->result.var), call);
}
if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) {
OBJ_RELEASE(Z_OBJ(call->This));
@ -94308,7 +94324,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_S
USE_OPLINE
zend_execute_data *call = EX(call);
zend_closure_from_frame(EX_VAR(opline->result.var), call);
if (opline->extended_value != (uint32_t)-1) {
zend_object *closure = CACHED_PTR(opline->extended_value);
if (closure) {
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
} else {
zval *closure_zv = zend_hash_index_lookup(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func);
if (Z_TYPE_P(closure_zv) == IS_NULL) {
zend_closure_from_frame(closure_zv, call);
}
ZEND_ASSERT(Z_TYPE_P(closure_zv) == IS_OBJECT);
closure = Z_OBJ_P(closure_zv);
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
CACHE_PTR(opline->extended_value, closure);
}
} else {
zend_closure_from_frame(EX_VAR(opline->result.var), call);
}
if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) {
OBJ_RELEASE(Z_OBJ(call->This));

2
Zend/zend_vm_opcodes.c

@ -439,7 +439,7 @@ static uint32_t zend_vm_opcodes_flags[211] = {
0x00000101,
0x00000101,
0x00000101,
0x00000101,
0x01040101,
0x00002001,
0x00000101,
0x00000100,

10
ext/dom/tests/registerPhpFunctionNS.phpt

@ -62,14 +62,14 @@ $xpath->registerPhpFunctionNS('urn:bar', 'test', 'strtolower');
var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
?>
--EXPECT--
--EXPECTF--
--- Legit cases: global function callable ---
object(DOMNodeList)#5 (1) {
["length"]=>
int(1)
}
--- Legit cases: string callable ---
object(DOMNodeList)#5 (1) {
object(DOMNodeList)#%d (1) {
["length"]=>
int(1)
}
@ -79,12 +79,12 @@ array(1) {
[0]=>
string(15) "https://PHP.net"
}
object(DOMNodeList)#3 (1) {
object(DOMNodeList)#%d (1) {
["length"]=>
int(0)
}
--- Legit cases: instance class method callable ---
object(DOMNodeList)#6 (1) {
object(DOMNodeList)#%d (1) {
["length"]=>
int(1)
}
@ -100,7 +100,7 @@ array(1) {
--- Legit cases: global function callable that returns nothing ---
string(15) "https://PHP.net"
--- Legit cases: multiple namespaces ---
object(DOMNodeList)#5 (1) {
object(DOMNodeList)#%d (1) {
["length"]=>
int(1)
}
Loading…
Cancel
Save