Make internal run_time_cache a persistent allocation - #15040
Conversation
522803f to
818f6d5
Compare
There was a problem hiding this comment.
What's the purpose of the uintptr_t casts being moved to intptr_t?
There was a problem hiding this comment.
Negative offsets with respect to CG(map_ptr_base). (Still positive compared to map_ptr_real_base).
But having negative offsets for the static ones allows allocating positive offsets for non-static map_ptrs during startup as well.
There was a problem hiding this comment.
Any reason these are mutable globals as opposed to thread-locals?
There was a problem hiding this comment.
That's the point - they're supposed to be allocated fully during startup. And startup is the same across all threads.
iluuu1994
left a comment
There was a problem hiding this comment.
I don't see any issues otherwise.
arnaud-lb
left a comment
There was a problem hiding this comment.
Nice!
As the internal runtime cache is never re-allocated after startup due to this change, it looks like that we could store a true pointer in ->run_time_cache instead of an offset, at least in non-ZTS builds?
|
@arnaud-lb Yes, in NTS that's possible. |
|
is this change meant to improve performance? if so, can the diff be measured? |
07a4cce to
477314f
Compare
a28a5f1 to
671d06d
Compare
|
I have benchmarked this patch on Symfony Demo with callgrind (with and without observer), using the following commands: So the patch makes some performance improvement for observer (eliminates call to zend_init_internal_run_time_cache() on each request) and "as a side effect" also makes slight improvement for normal execution (keeps "static" part of map_ptr space unchanged between requests). It would be great to saw this explanation in PR comments... |
There was a problem hiding this comment.
Why do we need this?
Why this is done only for non-ZTS build?
Will we have any problems with ZTS build?
Please use ZEND_MAP_PTR_INIT instead of ZEND_MAP_PTR() on left size.
There was a problem hiding this comment.
I'm using MAP_PTR_INIT for everything where I assign a value, but in this case I want to copy whatever binary data is, thus I found using ZEND_MAP_PTR directly more fitting and symmetric with the right side.
There was a problem hiding this comment.
Also, only need it in NTS, as in ZTS this uses a proper map_ptr which is set when the function is created and thus existing here.
In NTS the pointer is initialized later in the startup sequence and thus needs to be copied back.
There was a problem hiding this comment.
Can you explain why ZTS makes difference?
There was a problem hiding this comment.
We need per-thread pointers for ZTS (run-time cache is local to each request). But for NTS we can, as a slight optimization, just skip the indirect lookup via CG(map_ptr_base).
There was a problem hiding this comment.
Probably worth adding a comment here in code.
dstogov
left a comment
There was a problem hiding this comment.
The patch looks more or less good (I may miss some edge cases in implementation)
Please, answer my questions and make minor clean up.
You don't see any problems for Windows, where different workers have different copies of internal functions?
|
@dstogov This is not problematic on Windows, as this is fully local to each process. There's zero interaction with shared memory here. |
We also add zend_map_ptr_static, so that we do not incur the overhead of constantly recreating the internal run_time_cache pointers on each request. This mechanism might be extended for mutable_data of internal classes too.
671d06d to
87f038e
Compare
opcache persists CE-cache map_ptr offsets into SHM-interned class-name strings (zend_accel_get_class_name_map_ptr()), allocating them from the persisting process's CG(map_ptr_last). That numbering is process-local, and php-fpm de-synchronizes it across pools of one master: pool-level php_admin_value[extension] lines are loaded post-fork, and each internal function of such an extension claims a dynamic map_ptr slot at MINIT. A CE-cache offset baked by a pool without the extension then lands, in pools with the extension, inside the band of slots owned by internal-function run-time caches. When an fcall observer is registered, zend_init_internal_run_time_cache() fills every such slot with a zero-initialized arena slice each request, and zend_lookup_class_ex()'s CE-cache fast path - whose only guard is slot < CG(map_ptr_last) - returns that slice as a zend_class_entry *. The next static method call does zend_hash_find() on a zeroed function_table and segfaults at 4 * zend_string_hash(method_name). Backport the static map_ptr region from PHP 8.4, where this class of bug is fixed by design: - 25d7616 (phpGH-15040): internal-function run-time caches are allocated from a separate static region via ZEND_MAP_PTR_NEW_STATIC() instead of the per-process dynamic counter, so pool-local extension MINIT no longer advances CG(map_ptr_last) and SHM-persisted CE-cache offsets stay consistent across pools. - The map_ptr_static preload handling from 53fa98e (phpGH-17835): opcache's preload_load() re-allocates the map_ptr base itself and must account for the static region, otherwise the heap is corrupted under opcache.preload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
We also add zend_map_ptr_static, so that we do not incur the overhead of constantly recreating the internal run_time_cache pointers on each request when observers are enabled.
Additionally it saves a bit of zeroing of the map_ptrs on each request, independently of whether observers are actually enabled or not, given that they're below the zend_map_ptr_static_size threshold now.
Comparing instruction counts, it shows about 0.1% improvement without observers.
This mechanism might be extended for mutable_data of internal classes too. And possibly also for preloaded code.