[PATCH v8 1/2] malloc: add tcache support for large chunk caching
Cupertino Miranda
cupertino.miranda@oracle.com
Fri May 30 23:13:34 GMT 2025
Hi Wilco,
Unfortunately locally I did not get that performance difference.
I will work on your suggestions very closely to avoid further delays
having this patch approved.
Sorry for the setback.
I realized the mistake in using tcache_index in do_set_tcache_max. Of
course it would not work ... tcache == NULL. :(
Cheers,
Cupertino
On 30-05-2025 18:29, Wilco Dijkstra wrote:
> Hi Cupertino,
>
> Unfortunately the latest version is significantly slower on the tcache bench - 21% on
> latency... It now shares more code between the small and large tcache cases which is
> what we need to avoid. For example, tcache_index should never be used on fast paths
> that are for the small tcache. It also doesn't act like an index, which causes buggy code
> in do_set_tcache_max.
>
>
> +# define TCACHE_SMALL_BINS 64
> +# define TCACHE_LARGE_BINS 12 /* Up to 4M chunks */
> +# define TCACHE_MAX_BINS (TCACHE_SMALL_BINS + TCACHE_LARGE_BINS)
> +# define MAX_TCACHE_SMALL_SIZE tidx2usize (TCACHE_MAX_BINS-1)
>
> Small bins surely???
>
> +tcache_index (size_t bytes)
> {
> - if (tc_idx < mp_.tcache_bins
> - && tcache != NULL
> - && tcache->counts[tc_idx] > 0)
> - return true;
> + size_t tc_idx = csize2tidx (bytes);
> +
> + if (__glibc_unlikely (tcache == NULL))
> + return TCACHE_MAX_BINS;
> +
> + if (__glibc_likely (tc_idx < TCACHE_SMALL_BINS))
> + return tc_idx;
> else
> - return false;
> + {
> + tc_idx = large_csize2tidx (bytes);
> + return tc_idx;
> + }
> + return TCACHE_MAX_BINS;
>
> That looks like dead code? It feels wrong not to check mp_.tcache_max_bytes
> since it means we're always going to call tcache_get_large for large blocks
> even if they are disabled. However it's best not to use tcache_index at all.
>
>
> __libc_malloc (size_t bytes)
> {
> #if USE_TCACHE
> - size_t tc_idx = usize2tidx (bytes);
> + size_t nb = checked_request2size (bytes);
> + size_t tc_idx = tcache_index (nb);
>
> - if (tcache_available (tc_idx))
> - return tag_new_usable (tcache_get (tc_idx));
> + if (tc_idx < TCACHE_MAX_BINS
> + && tcache->entries[tc_idx] != NULL)
> + {
> + if (tc_idx < TCACHE_SMALL_BINS)
> + return tag_new_usable (tcache_get (tc_idx));
> + else
> + {
> + void *victim = tcache_get_large (tc_idx, nb);
> + if (victim != NULL)
> + return tag_new_usable (victim);
> + }
> + }
>
> To avoid slowdowns due to tcache_index, this should be written like:
>
> if (size <= TCACHE_SMALL_SIZE)
> {
> size_t tc_idx = csize2tidx (bytes);
> if (tcache != NULL && tcache->entries[tc_idx] != NULL)
> return tag_new_usable (tcache_get (tc_idx));
> }
> else if (size <= mp_.tcache_max_bytes && tcache != NULL)
> {
> size_t tc_idx = large_csize2tidx (bytes);
> void *victim = tcache_get_large (tc_idx, nb);
> if (victim != NULL)
> return tag_new_usable (victim);
> }
>
> This way you clearly separate the 2 cases and get optimal code for both.
> We can then optimize the tcache != NULL checks like in calloc.
>
>
> @@ -3428,10 +3529,10 @@ __libc_free (void *mem)
> check_inuse_chunk (arena_for_chunk (p), p);
>
> #if USE_TCACHE
> - size_t tc_idx = csize2tidx (size);
> -
> - if (__glibc_likely (tcache != NULL && tc_idx < mp_.tcache_bins))
> + if (__glibc_likely (tcache != NULL && size < mp_.tcache_max_bytes))
> {
> + size_t tc_idx = tcache_index (size);
>
> Using tcache_index here is *really* bad for performance...
>
>
> @@ -3440,7 +3541,15 @@ __libc_free (void *mem)
> return tcache_double_free_verify (e);
>
> if (__glibc_likely (tcache->counts[tc_idx] < mp_.tcache_count))
> - return tcache_put (p, tc_idx);
> + {
> + if (__glibc_likely (tc_idx < TCACHE_SMALL_BINS))
> + return tcache_put (p, tc_idx);
> + else if (!chunk_is_mmapped (p))
> + {
> + size_t tc_idx = tcache_index (size);
> + return tcache_put_large (p, tc_idx);
> + }
>
> Same as for malloc, just keep things simple and keep both cases completely
> separate.
>
>
> @@ -3825,21 +3919,24 @@ __libc_calloc (size_t n, size_t elem_size)
> }
>
> #if USE_TCACHE
> - size_t tc_idx = usize2tidx (bytes);
> - if (__glibc_likely (tc_idx < mp_.tcache_bins))
> - {
> - if (__glibc_unlikely (tcache == NULL))
> - return tcache_calloc_init (bytes);
> -
> - if (__glibc_likely (tcache->entries[tc_idx] != NULL))
> - {
> - void *mem = tcache_get (tc_idx);
> + size_t nb = checked_request2size (bytes);
> + size_t tc_idx = tcache_index (nb);
>
> - if (__glibc_unlikely (mtag_enabled))
> - return tag_new_zero_region (mem, memsize (mem2chunk (mem)));
> + void *mem = NULL;
> + if (__glibc_likely (tc_idx < TCACHE_MAX_BINS
> + && tcache->entries[tc_idx] != NULL))
> + {
> + if (tc_idx < TCACHE_SMALL_BINS)
> + mem = tcache_get (tc_idx);
> + else
> + mem = tcache_get_large (tc_idx, nb);
> + }
> + if (mem != NULL)
> + {
> + if (__glibc_unlikely (mtag_enabled))
> + return tag_new_zero_region (mem, memsize (mem2chunk (mem)));
>
> - return clear_memory ((INTERNAL_SIZE_T *) mem, tidx2usize (tc_idx));
> - }
> + return clear_memory ((INTERNAL_SIZE_T *) mem, memsize (mem2chunk (mem)));
>
>
> Calloc has the same issue as malloc with tcache_index. It also adds code outside the tcache
> paths and removes optimizations for the small tcache case. The extra assignment and check for
> NULL is unnecessary - you only need that for the large tcache path. Also large tcache should not
> use clear_memory since the size is always too large for it, so it just adds unnecessary overheads.
>
>
> @@ -3946,7 +4043,7 @@ _int_malloc (mstate av, size_t bytes)
> /* While we're here, if we see other chunks of the same size,
> stash them in the tcache. */
> size_t tc_idx = csize2tidx (nb);
> - if (tcache != NULL && tc_idx < mp_.tcache_bins)
> + if (tcache != NULL && nb < mp_.tcache_max_bytes)
>
> This is not correct - this is only for small tcache (and will remain so).
>
> @@ -4007,7 +4104,7 @@ _int_malloc (mstate av, size_t bytes)
> /* While we're here, if we see other chunks of the same size,
> stash them in the tcache. */
> size_t tc_idx = csize2tidx (nb);
> - if (tcache != NULL && tc_idx < mp_.tcache_bins)
> + if (tcache != NULL && tc_idx < mp_.tcache_max_bytes)
>
> Likewise.
>
> @@ -4069,7 +4166,7 @@ _int_malloc (mstate av, size_t bytes)
> #if USE_TCACHE
> INTERNAL_SIZE_T tcache_nb = 0;
> size_t tc_idx = csize2tidx (nb);
> - if (tcache != NULL && tc_idx < mp_.tcache_bins)
> + if (tcache != NULL && nb < mp_.tcache_max_bytes)
>
> Likewise (while we could possibly add large tcache support here in the future, it's likely
> a bad idea to preload tcache with large blocks).
>
>
> @@ -5482,13 +5579,16 @@ do_set_arena_max (size_t value)
> static __always_inline int
> do_set_tcache_max (size_t value)
> {
> - if (value <= MAX_TCACHE_SIZE)
> + size_t nb = request2size (value);
> + size_t index = tcache_index (nb);
> + LIBC_PROBE (memory_tunable_tcache_max_bytes, 2, value, mp_.tcache_max_bytes);
> +
> + if (index < TCACHE_MAX_BINS)
> {
> - LIBC_PROBE (memory_tunable_tcache_max_bytes, 2, value, mp_.tcache_max_bytes);
> - mp_.tcache_max_bytes = value;
> - mp_.tcache_bins = csize2tidx (request2size(value)) + 1;
> + mp_.tcache_max_bytes = nb;
> return 1;
>
> This won't work, now we will never set mp_.tcache_max_bytes...
>
> This also no longer sets mp_.tcache_bins which we still need for the above cases (after the
> counts are reversed we can just use TCACHE_SMALL_BINS).
>
> Cheers,
> Wilco
More information about the Libc-alpha
mailing list