ladybird/Tests/AK/TestHashTable.cpp
Jelle Raaijmakers c08d137fcd AK: Reimplement HashTable with smart linear probing
Instead of rehashing on collisions, we use Robin Hood hashing: a simple
linear probe where we keep track of the distance between the bucket and
its ideal position. On insertion, we allow a new bucket to "steal" the
position of "rich" buckets (those near their ideal position) and move
them further down.

On removal, we shift buckets back up into the freed slot, decrementing
their distance while doing so.

This behavior automatically optimizes the number of required probes for
any value, and removes the need for periodic rehashing (except when
expanding the capacity).
2023-02-17 22:29:51 -07:00

384 lines
9.6 KiB
C++

/*
* Copyright (c) 2021, thislooksfun <tlf@thislooks.fun>
* Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibTest/TestCase.h>
#include <AK/DeprecatedString.h>
#include <AK/HashTable.h>
#include <AK/NonnullOwnPtr.h>
TEST_CASE(construct)
{
using IntTable = HashTable<int>;
EXPECT(IntTable().is_empty());
EXPECT_EQ(IntTable().size(), 0u);
}
TEST_CASE(basic_move)
{
HashTable<int> foo;
foo.set(1);
EXPECT_EQ(foo.size(), 1u);
auto bar = move(foo);
EXPECT_EQ(bar.size(), 1u);
EXPECT_EQ(foo.size(), 0u);
foo = move(bar);
EXPECT_EQ(bar.size(), 0u);
EXPECT_EQ(foo.size(), 1u);
}
TEST_CASE(move_is_not_swap)
{
HashTable<int> foo;
foo.set(1);
HashTable<int> bar;
bar.set(2);
foo = move(bar);
EXPECT(foo.contains(2));
EXPECT(!bar.contains(1));
EXPECT_EQ(bar.size(), 0u);
}
TEST_CASE(populate)
{
HashTable<DeprecatedString> strings;
strings.set("One");
strings.set("Two");
strings.set("Three");
EXPECT_EQ(strings.is_empty(), false);
EXPECT_EQ(strings.size(), 3u);
}
TEST_CASE(range_loop)
{
HashTable<DeprecatedString> strings;
EXPECT_EQ(strings.set("One"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.set("Two"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.set("Three"), AK::HashSetResult::InsertedNewEntry);
int loop_counter = 0;
for (auto& it : strings) {
EXPECT_EQ(it.is_null(), false);
++loop_counter;
}
EXPECT_EQ(loop_counter, 3);
}
TEST_CASE(table_remove)
{
HashTable<DeprecatedString> strings;
EXPECT_EQ(strings.set("One"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.set("Two"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.set("Three"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.remove("One"), true);
EXPECT_EQ(strings.size(), 2u);
EXPECT(strings.find("One") == strings.end());
EXPECT_EQ(strings.remove("Three"), true);
EXPECT_EQ(strings.size(), 1u);
EXPECT(strings.find("Three") == strings.end());
EXPECT(strings.find("Two") != strings.end());
}
TEST_CASE(remove_all_matching)
{
HashTable<int> ints;
ints.set(1);
ints.set(2);
ints.set(3);
ints.set(4);
EXPECT_EQ(ints.size(), 4u);
EXPECT_EQ(ints.remove_all_matching([&](int value) { return value > 2; }), true);
EXPECT_EQ(ints.remove_all_matching([&](int) { return false; }), false);
EXPECT_EQ(ints.size(), 2u);
EXPECT(ints.contains(1));
EXPECT(ints.contains(2));
EXPECT_EQ(ints.remove_all_matching([&](int) { return true; }), true);
EXPECT(ints.is_empty());
EXPECT_EQ(ints.remove_all_matching([&](int) { return true; }), false);
}
TEST_CASE(case_insensitive)
{
HashTable<DeprecatedString, CaseInsensitiveStringTraits> casetable;
EXPECT_EQ(DeprecatedString("nickserv").to_lowercase(), DeprecatedString("NickServ").to_lowercase());
EXPECT_EQ(casetable.set("nickserv"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(casetable.set("NickServ"), AK::HashSetResult::ReplacedExistingEntry);
EXPECT_EQ(casetable.size(), 1u);
}
TEST_CASE(many_strings)
{
HashTable<DeprecatedString> strings;
for (int i = 0; i < 999; ++i) {
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
}
EXPECT_EQ(strings.size(), 999u);
for (int i = 0; i < 999; ++i) {
EXPECT_EQ(strings.remove(DeprecatedString::number(i)), true);
}
EXPECT_EQ(strings.is_empty(), true);
}
TEST_CASE(many_collisions)
{
struct StringCollisionTraits : public GenericTraits<DeprecatedString> {
static unsigned hash(DeprecatedString const&) { return 0; }
};
HashTable<DeprecatedString, StringCollisionTraits> strings;
for (int i = 0; i < 999; ++i) {
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
}
EXPECT_EQ(strings.set("foo"), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.size(), 1000u);
for (int i = 0; i < 999; ++i) {
EXPECT_EQ(strings.remove(DeprecatedString::number(i)), true);
}
EXPECT(strings.find("foo") != strings.end());
}
TEST_CASE(space_reuse)
{
struct StringCollisionTraits : public GenericTraits<DeprecatedString> {
static unsigned hash(DeprecatedString const&) { return 0; }
};
HashTable<DeprecatedString, StringCollisionTraits> strings;
// Add a few items to allow it to do initial resizing.
EXPECT_EQ(strings.set("0"), AK::HashSetResult::InsertedNewEntry);
for (int i = 1; i < 5; ++i) {
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.remove(DeprecatedString::number(i - 1)), true);
}
auto capacity = strings.capacity();
for (int i = 5; i < 999; ++i) {
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
EXPECT_EQ(strings.remove(DeprecatedString::number(i - 1)), true);
}
EXPECT_EQ(strings.capacity(), capacity);
}
TEST_CASE(basic_remove)
{
HashTable<int> table;
table.set(1);
table.set(2);
table.set(3);
EXPECT_EQ(table.remove(3), true);
EXPECT_EQ(table.remove(3), false);
EXPECT_EQ(table.size(), 2u);
EXPECT_EQ(table.remove(1), true);
EXPECT_EQ(table.remove(1), false);
EXPECT_EQ(table.size(), 1u);
EXPECT_EQ(table.remove(2), true);
EXPECT_EQ(table.remove(2), false);
EXPECT_EQ(table.size(), 0u);
}
TEST_CASE(basic_contains)
{
HashTable<int> table;
table.set(1);
table.set(2);
table.set(3);
EXPECT_EQ(table.contains(1), true);
EXPECT_EQ(table.contains(2), true);
EXPECT_EQ(table.contains(3), true);
EXPECT_EQ(table.contains(4), false);
EXPECT_EQ(table.remove(3), true);
EXPECT_EQ(table.contains(3), false);
EXPECT_EQ(table.contains(1), true);
EXPECT_EQ(table.contains(2), true);
EXPECT_EQ(table.remove(2), true);
EXPECT_EQ(table.contains(2), false);
EXPECT_EQ(table.contains(3), false);
EXPECT_EQ(table.contains(1), true);
EXPECT_EQ(table.remove(1), true);
EXPECT_EQ(table.contains(1), false);
}
TEST_CASE(capacity_leak)
{
HashTable<int> table;
for (size_t i = 0; i < 10000; ++i) {
table.set(i);
table.remove(i);
}
EXPECT(table.capacity() < 100u);
}
TEST_CASE(non_trivial_type_table)
{
HashTable<NonnullOwnPtr<int>> table;
table.set(make<int>(3));
table.set(make<int>(11));
for (int i = 0; i < 1'000; ++i) {
table.set(make<int>(-i));
}
for (int i = 0; i < 10'000; ++i) {
table.set(make<int>(i));
table.remove(make<int>(i));
}
EXPECT_EQ(table.remove_all_matching([&](auto&) { return true; }), true);
EXPECT(table.is_empty());
EXPECT_EQ(table.remove_all_matching([&](auto&) { return true; }), false);
}
TEST_CASE(floats)
{
HashTable<float> table;
table.set(0);
table.set(1.0f);
table.set(2.0f);
EXPECT_EQ(table.size(), 3u);
EXPECT(table.contains(0));
EXPECT(table.contains(1.0f));
EXPECT(table.contains(2.0f));
}
TEST_CASE(doubles)
{
HashTable<double> table;
table.set(0);
table.set(1.0);
table.set(2.0);
EXPECT_EQ(table.size(), 3u);
EXPECT(table.contains(0));
EXPECT(table.contains(1.0));
EXPECT(table.contains(2.0));
}
TEST_CASE(reinsertion)
{
OrderedHashTable<DeprecatedString> map;
map.set("ytidb::LAST_RESULT_ENTRY_KEY");
map.set("__sak");
map.remove("__sak");
map.set("__sak");
}
TEST_CASE(clear_with_capacity_when_empty)
{
HashTable<int> map;
map.clear_with_capacity();
map.set(0);
map.set(1);
VERIFY(map.size() == 2);
}
TEST_CASE(iterator_removal)
{
HashTable<int> map;
map.set(0);
map.set(1);
auto it = map.begin();
map.remove(it);
EXPECT_EQ(it, map.end());
EXPECT_EQ(map.size(), 1u);
}
TEST_CASE(ordered_insertion_and_deletion)
{
OrderedHashTable<int> table;
EXPECT_EQ(table.set(0), HashSetResult::InsertedNewEntry);
EXPECT_EQ(table.set(1), HashSetResult::InsertedNewEntry);
EXPECT_EQ(table.set(2), HashSetResult::InsertedNewEntry);
EXPECT_EQ(table.set(3), HashSetResult::InsertedNewEntry);
EXPECT_EQ(table.size(), 4u);
auto expect_table = [](OrderedHashTable<int>& table, Span<int> values) {
auto index = 0u;
for (auto it = table.begin(); it != table.end(); ++it, ++index) {
EXPECT_EQ(*it, values[index]);
EXPECT(table.contains(values[index]));
}
};
expect_table(table, Array<int, 4> { 0, 1, 2, 3 });
EXPECT(table.remove(0));
EXPECT(table.remove(2));
EXPECT(!table.remove(4));
EXPECT_EQ(table.size(), 2u);
expect_table(table, Array<int, 2> { 1, 3 });
}
TEST_CASE(ordered_deletion_and_reinsertion)
{
OrderedHashTable<int> table;
table.set(1);
table.set(3);
table.remove(1);
EXPECT_EQ(table.size(), 1u);
// By adding 1 again but this time in a different position, we
// test whether the bucket's neighbours are reset properly.
table.set(1);
EXPECT_EQ(table.size(), 2u);
auto it = table.begin();
EXPECT_EQ(*it, 3);
++it;
EXPECT_EQ(*it, 1);
++it;
EXPECT_EQ(it, table.end());
}
TEST_CASE(ordered_pop)
{
OrderedHashTable<int> table;
table.set(1);
table.set(2);
table.set(3);
EXPECT_EQ(table.pop(), 3);
EXPECT_EQ(table.pop(), 2);
EXPECT_EQ(table.pop(), 1);
EXPECT(table.is_empty());
}
TEST_CASE(ordered_iterator_removal)
{
OrderedHashTable<int> map;
map.set(0);
map.set(1);
auto it = map.begin();
map.remove(it);
EXPECT_EQ(it, map.end());
EXPECT_EQ(map.size(), 1u);
}