cds_lfht_new() clarification - init vs min

Ondřej Surý ondrej at sury.org
Thu Jul 10 10:40:29 EDT 2025


> On 10. 7. 2025, at 16:05, Mathieu Desnoyers <mathieu.desnoyers at efficios.com> wrote:
> 
> On 2025-07-09 14:57, Ondřej Surý via lttng-dev wrote:
>> Hi,
>> as the answer to this might be useful to more people, I am asking here:
>> The cds_lfht_new documentation specifies 3 sizes.
>>  * cds_lfht_new - allocate a hash table.
>>  * @init_size: number of buckets to allocate initially. Must be power of two.
>>  * @min_nr_alloc_buckets: the minimum number of allocated buckets.
>>  *                        (must be power of two)
>>  * @max_nr_buckets: the maximum number of hash table buckets allowed.
>>  *                  (must be power of two, 0 is accepted, means
>>  *                  "infinite")
>> The max number of buckets is obvious, but the interaction between init
>> and min is confusing.
>> If I am reading the code right, then init_size < min_nr_alloc_buckets have no
>> effect, the buckets table will be at least 1 << min_nr_alloc_buckets.
> 
> You are correct that the documentation of interaction between init_size
> and min_nr_alloc_buckets should be improved. Especially the semantic
> of "allocated" vs "initialized" buckets.
> 
> Re-reading the code, here is my understanding:
> 
> - The init_size parameter is responsible for initializing a certain
>  number of buckets at hash table creation (during the call to
>  cds_lfht_new()) and for setting an initial resize target to the
>  init_size value. It allocates the buckets memory _and_ chains
>  those buckets into the hash table.
> 
> - The min_nr_alloc_buckets only acts as a lower bound below which
>  the hash table resize algorithm won't free the _memory_ used for
>  buckets below that threshold and will keep the buckets allocated even
>  when the hash table size shrinks. It does not affect the fact buckets
>  are unlinked from the hash table when it shrinks below that threshold
>  though, it only keeps their memory allocated.
> 
>  This parameter only applies when an explicit cds_lfht_resize,
>  cds_lfht_resize_lazy_count are done, or when the hash table is
>  created with the CDS_LFHT_AUTO_RESIZE flag.
> 
> So my understanding does not match yours. In case I missed something,
> what is leading you conclude that the
> "buckets table will be at least 1 << min_nr_alloc_buckets"
> at hash table creation when init_size < min_nr_alloc_buckets ?

It's not so obvious from the mmap implementation, but the mm-order has:

static
void cds_lfht_alloc_bucket_table(struct cds_lfht *ht, unsigned long order)
{
        if (order == 0) {
                ht->tbl_order[0] = ht->alloc->calloc(ht->alloc->state,
                        ht->min_nr_alloc_buckets, sizeof(struct cds_lfht_node));
                urcu_posix_assert(ht->tbl_order[0]);
        } else if (order > ht->min_alloc_buckets_order) {
                ht->tbl_order[order] = ht->alloc->calloc(ht->alloc->state,
                        1UL << (order -1), sizeof(struct cds_lfht_node));
                urcu_posix_assert(ht->tbl_order[order]);
        }
        /* Nothing to do for 0 < order && order <= ht->min_alloc_buckets_order */
}

Similar code is in the mm-chunk.  The mmap allocates largest mmap and then it marks
the pages as needed, so it is bit hidden behind mmap semantics, but it also populates
min_nr_alloc_buckets:

                /* large table */
                ht->tbl_mmap = memory_map(ht->max_nr_buckets
                        * sizeof(*ht->tbl_mmap));
                memory_populate(ht->tbl_mmap,
                        ht->min_nr_alloc_buckets * sizeof(*ht->tbl_mmap));

and cds_lfht_alloc_bucket_table(..., 0) is called when initializing the from cds_lfht_new(),
so my assumption was that the buckets table is .tbl_<impl> and that's always at least
min_nr_alloc_buckets large.

The loop in cds_lfht_create_bucket() then does not change the underlying table size,
but it doesn't initialize buckets for orders > init size & < min_nr_alloc_size.

>> But what happens if init_size > min_nr_alloc_buckets? It feels like it will work
>> as expected if you pre-populate the table, but if you use it "normally", e.g. there
>> could be single add / del, the table will shrink immediately.
> 
> In that case, cds_lfht_new would allocate and initialize buckets based
> on init_size, and set an initial resize_target value based on init_size.
> 
> For a hash table created with the CDS_LFHT_AUTO_RESIZE flag, indeed if
> the init_size > min_nr_alloc_buckets, doing a add/del will indeed
> trigger a hash table auto-resize, which will unlink and free buckets
> between min_nr_alloc_buckets and init_size, and will just unlink buckets
> (not free memory) below min_nr_alloc_buckets.
> 
> So it does not make much sense to have a large init_size value for an
> auto-resize hash table, because it will very undo initialization
> and allocation soon after creation when the first items are added
> and removed.

However, it could make sense to have init_size smaller than min_nr_alloc_size
to make the bucket initialization deferred, but until we reach min_nr... the table
will never auto-shrink.

> For a hash table created without auto-resize, then the behavior depends
> on the explicit resizes done with cds_lfht_resize,
> cds_lfht_resize_lazy_grow, and cds_lfht_resize_lazy_count, so it makes
> more sense to have a larger init_size value (even larger than
> min_nr_alloc_buckets).
> 
> Does this explanation help ?

Absolutely, I was also thinking inside the CDS_LFHT_AUTO_RESIZE flag
because it is ubiquitous in our code and you made me realize that the resizes
could be done automatically.

Ondrej
--
Ondřej Surý (He/Him)
ondrej at sury.org




More information about the lttng-dev mailing list