From cc62ddac5e5c43651384b1ad830a64402b855da2 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:31:21 +0000 Subject: [PATCH] fix(zend): free each thread's strtod Bigint pool Bfree recycles a Bigint onto a per-thread freelist instead of releasing it, and zend_shutdown_strtod() only ever reaches the main thread's pool. Under ZTS every other thread that formatted or parsed a double left its Bigints behind when its storage went: measured as 68 bytes definitely lost per worker thread for a single sprintf('%6.3f'), scaling linearly with the number of threads. The freelist teardown now takes the state to free, because TSRM runs a thread's storage destructor from whichever thread performs the shutdown, and EG() there belongs to somebody else. --- Zend/zend.c | 5 +++++ Zend/zend_strtod.c | 35 +++++++++++++++++++++++++---------- Zend/zend_strtod.h | 3 +++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Zend/zend.c b/Zend/zend.c index b1b4e3b2cc68..2d2918e285d0 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -876,6 +876,11 @@ static void executor_globals_dtor(zend_executor_globals *executor_globals) /* {{ zend_hash_destroy(executor_globals->zend_constants); free(executor_globals->zend_constants); } + + /* Every thread pools its own Bigints, and zend_shutdown_strtod() reaches only + * the main thread's: without this a thread that ever formatted or parsed a + * double leaves them behind when its storage goes. */ + zend_strtod_state_dtor(&executor_globals->strtod_state); } /* }}} */ diff --git a/Zend/zend_strtod.c b/Zend/zend_strtod.c index f0a15a2f4f47..b4bfa14e22f7 100644 --- a/Zend/zend_strtod.c +++ b/Zend/zend_strtod.c @@ -545,8 +545,8 @@ Bigint { static Bigint *freelist[Kmax+1]; #endif -static void destroy_freelist(void); -static void free_p5s(void); +static void destroy_freelist(zend_strtod_state *state); +static void free_p5s(zend_strtod_state *state); #ifdef MULTIPLE_THREADS static MUTEX_T dtoa_mutex; @@ -555,11 +555,21 @@ static MUTEX_T pow5mult_mutex; ZEND_API int zend_shutdown_strtod(void) /* {{{ */ { - destroy_freelist(); - free_p5s(); + zend_strtod_state_dtor(&EG(strtod_state)); return 1; } +/* }}} */ + +/* Frees the Bigints one thread pooled. Bfree recycles rather than releasing, so + * every thread that formats or parses a double keeps a handful for its life. The + * state travels as an argument because TSRM runs a thread's storage dtor from + * whichever thread performs the shutdown, and EG() there is somebody else's. */ +ZEND_API void zend_strtod_state_dtor(zend_strtod_state *state) /* {{{ */ +{ + destroy_freelist(state); + free_p5s(state); +} /* }}} */ static Bigint * @@ -4605,33 +4615,38 @@ ZEND_API char *zend_gcvt(double value, int ndigit, char dec_point, char exponent return (buf); } -static void destroy_freelist(void) +/* The two pools are reached through a thread's own state from here on, so the + * EG() shorthands above must not swallow the field names. */ +#undef freelist +#undef p5s + +static void destroy_freelist(zend_strtod_state *state) { int i; Bigint *tmp; ACQUIRE_DTOA_LOCK(0) for (i = 0; i <= Kmax; i++) { - Bigint **listp = &freelist[i]; + Bigint **listp = &state->freelist[i]; while ((tmp = *listp) != NULL) { *listp = tmp->next; FREE(tmp); } - freelist[i] = NULL; + state->freelist[i] = NULL; } FREE_DTOA_LOCK(0) } -static void free_p5s(void) +static void free_p5s(zend_strtod_state *state) { Bigint **listp, *tmp; ACQUIRE_DTOA_LOCK(1) - listp = &p5s; + listp = &state->p5s; while ((tmp = *listp) != NULL) { *listp = tmp->next; FREE(tmp); } - p5s = NULL; + state->p5s = NULL; FREE_DTOA_LOCK(1) } diff --git a/Zend/zend_strtod.h b/Zend/zend_strtod.h index fa557ed2a0c3..5555575c188e 100644 --- a/Zend/zend_strtod.h +++ b/Zend/zend_strtod.h @@ -37,6 +37,9 @@ ZEND_API double zend_hex_strtod(const char *str, const char **endptr); ZEND_API double zend_oct_strtod(const char *str, const char **endptr); ZEND_API double zend_bin_strtod(const char *str, const char **endptr); ZEND_API int zend_shutdown_strtod(void); +/* Releases what one thread pooled. Called for every thread's state, not only the + * one zend_shutdown_strtod() reaches. */ +ZEND_API void zend_strtod_state_dtor(zend_strtod_state *state); END_EXTERN_C() /* double limits */