authorgravatar for sentientwaffle@gmail.comsentientwaffle <sentientwaffle@gmail.com> 2021-12-14 13:25:23-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-12-16 19:11:53-08:00
logef0566df7858df3a770a2b3112ca991358974be3
treebf4966f7cd3a7e6a988db12664f07756060053bb
parentd54ba76e40232f9e8e3f784e927f3138bdd97520

std: count hash_map tombstones as available

When entries are inserted and removed into a hash map at an equivalent rate (maintaining a mostly-consistent total count of entries), it should never need to be resized. But `HashMapUnmanaged.available` does not presently count tombstoned slots as "available", so this put/remove pattern eventually panics (assertion failure) when `available` reaches `0`. The solution implemented here is to count tombstoned slots as "available". Another approach (which hashbrown (https://github.com/rust-lang/hashbrown/blob/b3eaf32e608d1ec4c10963a4f495503d7f8a7ef5/src/raw/mod.rs#L1455-L1542) takes) would be to rehash all entries in place when there are too many tombstones. This is more complex but avoids an `O(n)` bad case when the hash map is full of many tombstones.

1 files changed, 45 insertions(+), 11 deletions(-)

lib/std/hash_map.zig+45-11
......@@ -1007,10 +1007,8 @@ pub fn HashMapUnmanaged(
10071007 metadata = self.metadata.? + idx;
10081008 }
10091009
1010 if (!metadata[0].isTombstone()) {
1011 assert(self.available > 0);
1012 self.available -= 1;
1013 }
1010 assert(self.available > 0);
1011 self.available -= 1;
10141012
10151013 const fingerprint = Metadata.takeFingerprint(hash);
10161014 metadata[0].fill(fingerprint);
......@@ -1112,10 +1110,12 @@ pub fn HashMapUnmanaged(
11121110 }
11131111 const mask = self.capacity() - 1;
11141112 const fingerprint = Metadata.takeFingerprint(hash);
1113 // Don't loop indefinitely when there are no empty slots.
1114 var limit = self.capacity();
11151115 var idx = @truncate(usize, hash & mask);
11161116
11171117 var metadata = self.metadata.? + idx;
1118 while (metadata[0].isUsed() or metadata[0].isTombstone()) {
1118 while ((metadata[0].isUsed() or metadata[0].isTombstone()) and limit != 0) {
11191119 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
11201120 const test_key = &self.keys()[idx];
11211121 // If you get a compile error on this line, it means that your generic eql
......@@ -1131,6 +1131,7 @@ pub fn HashMapUnmanaged(
11311131 }
11321132 }
11331133
1134 limit -= 1;
11341135 idx = (idx + 1) & mask;
11351136 metadata = self.metadata.? + idx;
11361137 }
......@@ -1288,11 +1289,12 @@ pub fn HashMapUnmanaged(
12881289 }
12891290 const mask = self.capacity() - 1;
12901291 const fingerprint = Metadata.takeFingerprint(hash);
1292 var limit = self.capacity();
12911293 var idx = @truncate(usize, hash & mask);
12921294
12931295 var first_tombstone_idx: usize = self.capacity(); // invalid index
12941296 var metadata = self.metadata.? + idx;
1295 while (metadata[0].isUsed() or metadata[0].isTombstone()) {
1297 while ((metadata[0].isUsed() or metadata[0].isTombstone()) and limit != 0) {
12961298 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
12971299 const test_key = &self.keys()[idx];
12981300 // If you get a compile error on this line, it means that your generic eql
......@@ -1314,6 +1316,7 @@ pub fn HashMapUnmanaged(
13141316 first_tombstone_idx = idx;
13151317 }
13161318
1319 limit -= 1;
13171320 idx = (idx + 1) & mask;
13181321 metadata = self.metadata.? + idx;
13191322 }
......@@ -1322,10 +1325,9 @@ pub fn HashMapUnmanaged(
13221325 // Cheap try to lower probing lengths after deletions. Recycle a tombstone.
13231326 idx = first_tombstone_idx;
13241327 metadata = self.metadata.? + idx;
1325 } else {
1326 // We're using a slot previously free.
1327 self.available -= 1;
13281328 }
1329 // We're using a slot previously free or a tombstone.
1330 self.available -= 1;
13291331
13301332 metadata[0].fill(fingerprint);
13311333 const new_key = &self.keys()[idx];
......@@ -1385,6 +1387,7 @@ pub fn HashMapUnmanaged(
13851387 self.keys()[idx] = undefined;
13861388 self.values()[idx] = undefined;
13871389 self.size -= 1;
1390 self.available += 1;
13881391 return true;
13891392 }
13901393
......@@ -1395,7 +1398,7 @@ pub fn HashMapUnmanaged(
13951398 @memset(@ptrCast([*]u8, self.metadata.?), 0, @sizeOf(Metadata) * self.capacity());
13961399 }
13971400
1398 // This counts the number of occupied slots, used + tombstones, which is
1401 // This counts the number of occupied slots (not counting tombstones), which is
13991402 // what has to stay under the max_load_percentage of capacity.
14001403 fn load(self: *const Self) Size {
14011404 const max_load = (self.capacity() * max_load_percentage) / 100;
......@@ -1587,7 +1590,6 @@ test "std.hash_map ensureUnusedCapacity with tombstones" {
15871590 while (i < 100) : (i += 1) {
15881591 try map.ensureUnusedCapacity(1);
15891592 map.putAssumeCapacity(i, i);
1590 // Remove to create tombstones that still count as load in the hashmap.
15911593 _ = map.remove(i);
15921594 }
15931595}
......@@ -1894,6 +1896,38 @@ test "std.hash_map putAssumeCapacity" {
18941896 try expectEqual(sum, 20);
18951897}
18961898
1899test "std.hash_map repeat putAssumeCapacity/remove" {
1900 var map = AutoHashMap(u32, u32).init(std.testing.allocator);
1901 defer map.deinit();
1902
1903 try map.ensureTotalCapacity(20);
1904 const limit = map.unmanaged.available;
1905
1906 var i: u32 = 0;
1907 while (i < limit) : (i += 1) {
1908 map.putAssumeCapacityNoClobber(i, i);
1909 }
1910
1911 // Repeatedly delete/insert an entry without resizing the map.
1912 // Put to different keys so entries don't land in the just-freed slot.
1913 i = 0;
1914 while (i < 10 * limit) : (i += 1) {
1915 try testing.expect(map.remove(i));
1916 if (i % 2 == 0) {
1917 map.putAssumeCapacityNoClobber(limit + i, i);
1918 } else {
1919 map.putAssumeCapacity(limit + i, i);
1920 }
1921 }
1922
1923 i = 9 * limit;
1924 while (i < 10 * limit) : (i += 1) {
1925 try expectEqual(map.get(limit + i), i);
1926 }
1927 try expectEqual(map.unmanaged.available, 0);
1928 try expectEqual(map.unmanaged.count(), limit);
1929}
1930
18971931test "std.hash_map getOrPut" {
18981932 var map = AutoHashMap(u32, u32).init(std.testing.allocator);
18991933 defer map.deinit();