Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std: hash_map: Better handling of no available slots case #7472

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 65 additions & 8 deletions lib/std/hash_map.zig
Expand Up @@ -315,6 +315,10 @@ pub fn HashMapUnmanaged(
return self.tombstone == 1;
}

pub fn isEmpty(self: Metadata) bool {
return self.used == 0 and self.tombstone == 0;
}

pub fn takeFingerprint(hash: Hash) FingerPrint {
const hash_bits = @typeInfo(Hash).Int.bits;
const fp_bits = @typeInfo(FingerPrint).Int.bits;
Expand Down Expand Up @@ -535,9 +539,10 @@ pub fn HashMapUnmanaged(
const mask = self.capacity() - 1;
const fingerprint = Metadata.takeFingerprint(hash);
var idx = @truncate(usize, hash & mask);
var probe_length = self.capacity();

var metadata = self.metadata.? + idx;
while (metadata[0].isUsed() or metadata[0].isTombstone()) {
while (probe_length > 0) : (probe_length -= 1) {
if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
const entry = &self.entries()[idx];
if (eqlFn(entry.key, key)) {
Expand Down Expand Up @@ -568,9 +573,11 @@ pub fn HashMapUnmanaged(
const mask = self.capacity() - 1;
const fingerprint = Metadata.takeFingerprint(hash);
var idx = @truncate(usize, hash & mask);
var probe_length = self.capacity();

var metadata = self.metadata.? + idx;
while (metadata[0].isUsed() or metadata[0].isTombstone()) {

while (probe_length > 0) : (probe_length -= 1) {
if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
const entry = &self.entries()[idx];
if (eqlFn(entry.key, key)) {
Expand All @@ -595,17 +602,26 @@ pub fn HashMapUnmanaged(
const mask = self.capacity() - 1;
const fingerprint = Metadata.takeFingerprint(hash);
var idx = @truncate(usize, hash & mask);
var probe_length = self.capacity();

var first_tombstone_idx: usize = self.capacity(); // invalid index
var metadata = self.metadata.? + idx;
while (metadata[0].isUsed() or metadata[0].isTombstone()) {

// Stop the probing if we wrap around.
while (probe_length > 0) : (probe_length -= 1) {
if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
// Update an existing entry.
const entry = &self.entries()[idx];
if (eqlFn(entry.key, key)) {
return GetOrPutResult{ .entry = entry, .found_existing = true };
}
} else if (first_tombstone_idx == self.capacity() and metadata[0].isTombstone()) {
} else if (metadata[0].isTombstone() and first_tombstone_idx == self.capacity()) {
// Recycle a tombstoned entry if no better slot is found.
first_tombstone_idx = idx;
} else if (metadata[0].isEmpty() and self.available > 0) {
// Take an empty entry, but only if we're not above the
// maximum load factor.
break;
}

idx = (idx + 1) & mask;
Expand All @@ -621,6 +637,8 @@ pub fn HashMapUnmanaged(
self.available -= 1;
}

assert(!metadata[0].isUsed());

metadata[0].fill(fingerprint);
const entry = &self.entries()[idx];
entry.* = .{ .key = key, .value = undefined };
Expand Down Expand Up @@ -649,9 +667,10 @@ pub fn HashMapUnmanaged(
const mask = self.capacity() - 1;
const fingerprint = Metadata.takeFingerprint(hash);
var idx = @truncate(usize, hash & mask);
var probe_length = self.capacity();

var metadata = self.metadata.? + idx;
while (metadata[0].isUsed() or metadata[0].isTombstone()) {
while (probe_length > 0) : (probe_length -= 1) {
if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
const entry = &self.entries()[idx];
if (eqlFn(entry.key, key)) {
Expand All @@ -678,9 +697,10 @@ pub fn HashMapUnmanaged(
const mask = self.capacity() - 1;
const fingerprint = Metadata.takeFingerprint(hash);
var idx = @truncate(usize, hash & mask);
var probe_length = self.capacity();

var metadata = self.metadata.? + idx;
while (metadata[0].isUsed() or metadata[0].isTombstone()) {
while (probe_length > 0) : (probe_length -= 1) {
if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
const entry = &self.entries()[idx];
if (eqlFn(entry.key, key)) {
Expand Down Expand Up @@ -1070,9 +1090,9 @@ test "std.hash_map put and remove loop in random order" {
}
}

test "std.hash_map remove one million elements in random order" {
test "std.hash_map remove a lot of elements in random order" {
const Map = AutoHashMap(u32, u32);
const n = 1000 * 1000;
const n = 10 * 1000;
var map = Map.init(std.heap.page_allocator);
defer map.deinit();

Expand Down Expand Up @@ -1238,3 +1258,40 @@ test "std.hash_map clone" {
testing.expect(copy.get(i).? == i * 10);
}
}

test "putAssumeCapacity with no available empty slots" {
// Identity mapping between the allocated slot index and the key.
const S = struct {
fn hash(key: u32) u64 {
return key;
}
fn eql(a: u32, b: u32) bool {
return a == b;
}
};
var map: HashMapUnmanaged(u32, u32, S.hash, S.eql, DefaultMaxLoadPercentage) = .{};
defer map.deinit(std.testing.allocator);

try map.ensureCapacity(std.testing.allocator, 5);
const avail = map.available;
// Fill all the available slots.
var i: usize = 0;
while (i < avail) : (i += 1) {
map.putAssumeCapacity(@intCast(u32, i), 0);
}
testing.expect(map.available == 0);
testing.expect(map.size == avail);
// Remove everything, all the usable slots are tombstones.
i = 0;
while (i < avail) : (i += 1) {
map.removeAssertDiscard(@intCast(u32, i));
}
testing.expect(map.available == 0);
testing.expect(map.size == 0);
// Try to fill the unused slots, since map.available is zero the insertion
// logic will recycle the next tombstoned entry that's available.
i = avail;
while (i < map.capacity()) : (i += 1) {
map.putAssumeCapacity(@intCast(u32, i), 0);
}
}