Skip to content

Commit 6e87883

Browse files
perf(profiling): remove indirect allocator forwarding
Forward PHP 8.4+ allocations and frees from a single ZendMMState read instead of loading a function pointer, making an indirect Rust call, and reloading the state. Preserve neighboring custom allocator support with a predictable previous-handler check. Use unchecked heap extraction in the callbacks: rinit stores the heap before they can be invoked, and rshutdown removes them before clearing it. This avoids an Option discriminant check on every allocation and free. Across six 60-second runs per binary, mean allocation throughput increased from 9,366,444/s to 10,018,262/s (+6.96%) and median throughput increased by 7.56%. A 60-second native sample reduced known forwarding-path self time from 3.599% to 2.438% (-32.3% relative). Validation: cargo test (22 passed); allocation sampling-distance, memory-peak, and GC PHPTs passed. https://datadoghq.atlassian.net/browse/PROF-15506
1 parent 241f72a commit 6e87883

2 files changed

Lines changed: 28 additions & 58 deletions

File tree

profiling/src/allocation/allocation_ge84.rs

Lines changed: 26 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,11 @@ pub struct ZendMMState {
3434
/// The engine's previous custom shutdown function, if there is one.
3535
prev_custom_mm_shutdown: Option<zend::VmMmCustomShutdownFn>,
3636
/// Safety: this function pointer is only allowed to point to
37-
/// `alloc_prof_prev_alloc()` when at the same time the
38-
/// `ZEND_MM_STATE.prev_custom_mm_alloc` is initialised to a valid function
39-
/// pointer, otherwise there will be dragons.
40-
alloc: unsafe fn(size_t) -> *mut c_void,
41-
/// Safety: this function pointer is only allowed to point to
4237
/// `alloc_prof_prev_realloc()` when at the same time the
4338
/// `ZEND_MM_STATE.prev_custom_mm_realloc` is initialised to a valid
4439
/// function pointer, otherwise there will be dragons.
4540
realloc: unsafe fn(*mut c_void, size_t) -> *mut c_void,
4641
/// Safety: this function pointer is only allowed to point to
47-
/// `alloc_prof_prev_free()` when at the same time the
48-
/// `ZEND_MM_STATE.prev_custom_mm_free` is initialised to a valid function
49-
/// pointer, otherwise there will be dragons.
50-
free: unsafe fn(*mut c_void),
51-
/// Safety: this function pointer is only allowed to point to
5242
/// `alloc_prof_prev_gc()` when at the same time the
5343
/// `ZEND_MM_STATE.prev_custom_mm_gc` is initialised to a valid function
5444
/// pointer, otherwise there will be dragons.
@@ -78,9 +68,7 @@ impl ZendMMState {
7868
prev_custom_mm_free: None,
7969
prev_custom_mm_gc: None,
8070
prev_custom_mm_shutdown: None,
81-
alloc: super::alloc_prof_panic_alloc,
8271
realloc: super::alloc_prof_panic_realloc,
83-
free: super::alloc_prof_panic_free,
8472
gc: alloc_prof_panic_gc,
8573
shutdown: alloc_prof_panic_shutdown,
8674
}
@@ -127,14 +115,10 @@ pub fn alloc_prof_rinit(heap_live_enabled: bool) {
127115
ptr::addr_of_mut!(zend_mm_state.prev_custom_mm_shutdown),
128116
);
129117
}
130-
zend_mm_state.alloc = alloc_prof_prev_alloc;
131-
zend_mm_state.free = alloc_prof_prev_free;
132118
zend_mm_state.realloc = alloc_prof_prev_realloc;
133119
zend_mm_state.gc = alloc_prof_prev_gc;
134120
zend_mm_state.shutdown = alloc_prof_prev_shutdown;
135121
} else {
136-
zend_mm_state.alloc = alloc_prof_orig_alloc;
137-
zend_mm_state.free = alloc_prof_orig_free;
138122
zend_mm_state.realloc = alloc_prof_orig_realloc;
139123
zend_mm_state.gc = alloc_prof_orig_gc;
140124
zend_mm_state.shutdown = alloc_prof_orig_shutdown;
@@ -303,7 +287,7 @@ unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void {
303287
#[cfg(feature = "debug_stats")]
304288
ALLOCATION_PROFILING_SIZE.fetch_add(len as u64, Relaxed);
305289

306-
let ptr = tls_zend_mm_state_get!(alloc)(len);
290+
let ptr = alloc_prof_forward_alloc(len);
307291

308292
// during startup, minit, rinit, ... current_execute_data is null
309293
// we are only interested in allocations during userland operations
@@ -318,27 +302,19 @@ unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void {
318302
ptr
319303
}
320304

321-
unsafe fn alloc_prof_prev_alloc(len: size_t) -> *mut c_void {
322-
// Safety: `ZEND_MM_STATE.prev_custom_mm_alloc` will be initialised in
323-
// `alloc_prof_rinit()` and only point to this function when
324-
// `prev_custom_mm_alloc` is also initialised.
325-
// Note: We use `.unwrap()` instead of `.unwrap_unchecked()` here because a
326-
// neighboring extension could misbehave. If that happens, we want a proper
327-
// panic with backtrace for debugging rather than undefined behavior.
328-
let alloc = tls_zend_mm_state_get!(prev_custom_mm_alloc).unwrap();
329-
#[cfg(php_debug)]
330-
{
331-
alloc(len, ptr::null(), 0, ptr::null(), 0)
305+
#[inline(always)]
306+
unsafe fn alloc_prof_forward_alloc(len: size_t) -> *mut c_void {
307+
let state = tls_zend_mm_state_copy!();
308+
if let Some(alloc) = state.prev_custom_mm_alloc {
309+
#[cfg(php_debug)]
310+
return alloc(len, ptr::null(), 0, ptr::null(), 0);
311+
#[cfg(not(php_debug))]
312+
return alloc(len);
332313
}
333-
#[cfg(not(php_debug))]
334-
alloc(len)
335-
}
336314

337-
unsafe fn alloc_prof_orig_alloc(len: size_t) -> *mut c_void {
338-
// Safety: `ZEND_MM_STATE.heap` will be initialised in `alloc_prof_rinit()` and custom ZendMM
339-
// handlers only point to this function after successful init. Using `unwrap_unchecked()` is
340-
// safe here as we have full control over ZendMM with no neighboring extensions.
341-
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
315+
// SAFETY: this callback is only invoked after rinit stores the heap and
316+
// before rshutdown clears it.
317+
let heap = state.heap.unwrap_unchecked();
342318
#[cfg(php_debug)]
343319
return zend::_zend_mm_alloc(heap, len, ptr::null(), 0, ptr::null(), 0);
344320
#[cfg(not(php_debug))]
@@ -375,7 +351,7 @@ fn alloc_prof_free_handler(heap_live_enabled: bool) -> zend::VmMmCustomFreeFn {
375351

376352
#[cfg(not(php_debug))]
377353
unsafe extern "C" fn alloc_prof_free_noop(ptr: *mut c_void) {
378-
tls_zend_mm_state_get!(free)(ptr);
354+
alloc_prof_forward_free(ptr);
379355
}
380356

381357
#[cfg(php_debug)]
@@ -386,7 +362,7 @@ unsafe extern "C" fn alloc_prof_free_noop(
386362
_orig_file: *const c_char,
387363
_orig_line: c_uint,
388364
) {
389-
tls_zend_mm_state_get!(free)(ptr);
365+
alloc_prof_forward_free(ptr);
390366
}
391367

392368
#[inline(always)]
@@ -395,30 +371,22 @@ unsafe fn alloc_prof_free_impl(ptr: *mut c_void) {
395371
if !ptr.is_null() {
396372
untrack_allocation(ptr);
397373
}
398-
tls_zend_mm_state_get!(free)(ptr);
374+
alloc_prof_forward_free(ptr);
399375
}
400376

401-
unsafe fn alloc_prof_prev_free(ptr: *mut c_void) {
402-
// Safety: `ZEND_MM_STATE.prev_custom_mm_free` will be initialised in
403-
// `alloc_prof_rinit()` and only point to this function when
404-
// `prev_custom_mm_free` is also initialised.
405-
// Note: We use `.unwrap()` instead of `.unwrap_unchecked()` here because a
406-
// neighboring extension could misbehave. If that happens, we want a proper
407-
// panic with backtrace for debugging rather than undefined behavior.
408-
let free = tls_zend_mm_state_get!(prev_custom_mm_free).unwrap();
409-
#[cfg(php_debug)]
410-
{
411-
free(ptr, core::ptr::null(), 0, core::ptr::null(), 0)
377+
#[inline(always)]
378+
unsafe fn alloc_prof_forward_free(ptr: *mut c_void) {
379+
let state = tls_zend_mm_state_copy!();
380+
if let Some(free) = state.prev_custom_mm_free {
381+
#[cfg(php_debug)]
382+
return free(ptr, core::ptr::null(), 0, core::ptr::null(), 0);
383+
#[cfg(not(php_debug))]
384+
return free(ptr);
412385
}
413-
#[cfg(not(php_debug))]
414-
free(ptr)
415-
}
416386

417-
unsafe fn alloc_prof_orig_free(ptr: *mut c_void) {
418-
// Safety: `ZEND_MM_STATE.heap` will be initialised in `alloc_prof_rinit()` and custom ZendMM
419-
// handlers only point to this function after successful init. Using `unwrap_unchecked()` is
420-
// safe here as we have full control over ZendMM with no neighboring extensions.
421-
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
387+
// SAFETY: this callback is only invoked after rinit stores the heap and
388+
// before rshutdown clears it.
389+
let heap = state.heap.unwrap_unchecked();
422390
#[cfg(php_debug)]
423391
return zend::_zend_mm_free(heap, ptr, core::ptr::null(), 0, core::ptr::null(), 0);
424392
#[cfg(not(php_debug))]

profiling/src/allocation/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ fn initialization_panic() -> ! {
275275
panic!("Allocation profiler was not initialized properly. Please fill an issue stating the PHP version and the backtrace from this panic.");
276276
}
277277

278+
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
278279
unsafe fn alloc_prof_panic_alloc(_len: size_t) -> *mut c_void {
279280
initialization_panic();
280281
}
@@ -283,6 +284,7 @@ unsafe fn alloc_prof_panic_realloc(_prev_ptr: *mut c_void, _len: size_t) -> *mut
283284
initialization_panic();
284285
}
285286

287+
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
286288
unsafe fn alloc_prof_panic_free(_ptr: *mut c_void) {
287289
initialization_panic();
288290
}

0 commit comments

Comments
 (0)