AK: Automatically shrink HashTable when removing entries

If the utilization of a HashTable (size vs capacity) goes below 20%,
we'll now shrink the table down to capacity = (size * 2).

This fixes an issue where tables would grow infinitely when inserting
and removing keys repeatedly. Basically, we would accumulate deleted
buckets with nothing reclaiming them, and eventually deciding that we
needed to grow the table (because we grow if used+deleted > limit!)

I found this because HashTable iteration was taking a suspicious amount
of time in Core::EventLoop::get_next_timer_expiration(). Turns out the
timer table kept growing in capacity over time. That made iteration
slower and slower since HashTable iterators visit every bucket.
This commit is contained in:
Andreas Kling 2022-03-06 19:26:04 +01:00
parent eb829924da
commit 9d8da1697e
Notes: sideshowbarker 2024-07-17 17:51:15 +09:00

View file

@ -402,6 +402,8 @@ public:
delete_bucket(bucket);
--m_size;
++m_deleted_count;
shrink_if_needed();
}
template<typename TUnaryPredicate>
@ -418,9 +420,9 @@ public:
if (removed_count) {
m_deleted_count += removed_count;
m_size -= removed_count;
return true;
}
return false;
shrink_if_needed();
return removed_count;
}
private:
@ -543,6 +545,19 @@ private:
[[nodiscard]] size_t used_bucket_count() const { return m_size + m_deleted_count; }
[[nodiscard]] bool should_grow() const { return ((used_bucket_count() + 1) * 100) >= (m_capacity * load_factor_in_percent); }
void shrink_if_needed()
{
// Shrink if less than 20% of buckets are used, but never going below 16.
// These limits are totally arbitrary and can probably be improved.
bool should_shrink = m_size * 5 < m_capacity && m_capacity > 16;
if (!should_shrink)
return;
// NOTE: We ignore memory allocation failure here, since we can continue
// just fine with an oversized table.
(void)try_rehash(m_size * 2);
}
void delete_bucket(auto& bucket)
{
bucket.slot()->~T();