From 9d8da1697e054c7b12210cb14451abc398add7ae Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 6 Mar 2022 19:26:04 +0100 Subject: [PATCH] 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. --- AK/HashTable.h | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/AK/HashTable.h b/AK/HashTable.h index 3cc0691c306..e01ab001fa9 100644 --- a/AK/HashTable.h +++ b/AK/HashTable.h @@ -402,6 +402,8 @@ public: delete_bucket(bucket); --m_size; ++m_deleted_count; + + shrink_if_needed(); } template @@ -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();