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(...@@ -1007,10 +1007,8 @@ pub fn HashMapUnmanaged(
1007 metadata = self.metadata.? + idx;1007 metadata = self.metadata.? + idx;
1008 }1008 }
10091009
1010 if (!metadata[0].isTombstone()) {1010 assert(self.available > 0);
1011 assert(self.available > 0);1011 self.available -= 1;
1012 self.available -= 1;
1013 }
10141012
1015 const fingerprint = Metadata.takeFingerprint(hash);1013 const fingerprint = Metadata.takeFingerprint(hash);
1016 metadata[0].fill(fingerprint);1014 metadata[0].fill(fingerprint);
...@@ -1112,10 +1110,12 @@ pub fn HashMapUnmanaged(...@@ -1112,10 +1110,12 @@ pub fn HashMapUnmanaged(
1112 }1110 }
1113 const mask = self.capacity() - 1;1111 const mask = self.capacity() - 1;
1114 const fingerprint = Metadata.takeFingerprint(hash);1112 const fingerprint = Metadata.takeFingerprint(hash);
1113 // Don't loop indefinitely when there are no empty slots.
1114 var limit = self.capacity();
1115 var idx = @truncate(usize, hash & mask);1115 var idx = @truncate(usize, hash & mask);
11161116
1117 var metadata = self.metadata.? + idx;1117 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) {
1119 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {1119 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
1120 const test_key = &self.keys()[idx];1120 const test_key = &self.keys()[idx];
1121 // If you get a compile error on this line, it means that your generic eql1121 // If you get a compile error on this line, it means that your generic eql
...@@ -1131,6 +1131,7 @@ pub fn HashMapUnmanaged(...@@ -1131,6 +1131,7 @@ pub fn HashMapUnmanaged(
1131 }1131 }
1132 }1132 }
11331133
1134 limit -= 1;
1134 idx = (idx + 1) & mask;1135 idx = (idx + 1) & mask;
1135 metadata = self.metadata.? + idx;1136 metadata = self.metadata.? + idx;
1136 }1137 }
...@@ -1288,11 +1289,12 @@ pub fn HashMapUnmanaged(...@@ -1288,11 +1289,12 @@ pub fn HashMapUnmanaged(
1288 }1289 }
1289 const mask = self.capacity() - 1;1290 const mask = self.capacity() - 1;
1290 const fingerprint = Metadata.takeFingerprint(hash);1291 const fingerprint = Metadata.takeFingerprint(hash);
1292 var limit = self.capacity();
1291 var idx = @truncate(usize, hash & mask);1293 var idx = @truncate(usize, hash & mask);
12921294
1293 var first_tombstone_idx: usize = self.capacity(); // invalid index1295 var first_tombstone_idx: usize = self.capacity(); // invalid index
1294 var metadata = self.metadata.? + idx;1296 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) {
1296 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {1298 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
1297 const test_key = &self.keys()[idx];1299 const test_key = &self.keys()[idx];
1298 // If you get a compile error on this line, it means that your generic eql1300 // If you get a compile error on this line, it means that your generic eql
...@@ -1314,6 +1316,7 @@ pub fn HashMapUnmanaged(...@@ -1314,6 +1316,7 @@ pub fn HashMapUnmanaged(
1314 first_tombstone_idx = idx;1316 first_tombstone_idx = idx;
1315 }1317 }
13161318
1319 limit -= 1;
1317 idx = (idx + 1) & mask;1320 idx = (idx + 1) & mask;
1318 metadata = self.metadata.? + idx;1321 metadata = self.metadata.? + idx;
1319 }1322 }
...@@ -1322,10 +1325,9 @@ pub fn HashMapUnmanaged(...@@ -1322,10 +1325,9 @@ pub fn HashMapUnmanaged(
1322 // Cheap try to lower probing lengths after deletions. Recycle a tombstone.1325 // Cheap try to lower probing lengths after deletions. Recycle a tombstone.
1323 idx = first_tombstone_idx;1326 idx = first_tombstone_idx;
1324 metadata = self.metadata.? + idx;1327 metadata = self.metadata.? + idx;
1325 } else {
1326 // We're using a slot previously free.
1327 self.available -= 1;
1328 }1328 }
1329 // We're using a slot previously free or a tombstone.
1330 self.available -= 1;
13291331
1330 metadata[0].fill(fingerprint);1332 metadata[0].fill(fingerprint);
1331 const new_key = &self.keys()[idx];1333 const new_key = &self.keys()[idx];
...@@ -1385,6 +1387,7 @@ pub fn HashMapUnmanaged(...@@ -1385,6 +1387,7 @@ pub fn HashMapUnmanaged(
1385 self.keys()[idx] = undefined;1387 self.keys()[idx] = undefined;
1386 self.values()[idx] = undefined;1388 self.values()[idx] = undefined;
1387 self.size -= 1;1389 self.size -= 1;
1390 self.available += 1;
1388 return true;1391 return true;
1389 }1392 }
13901393
...@@ -1395,7 +1398,7 @@ pub fn HashMapUnmanaged(...@@ -1395,7 +1398,7 @@ pub fn HashMapUnmanaged(
1395 @memset(@ptrCast([*]u8, self.metadata.?), 0, @sizeOf(Metadata) * self.capacity());1398 @memset(@ptrCast([*]u8, self.metadata.?), 0, @sizeOf(Metadata) * self.capacity());
1396 }1399 }
13971400
1398 // This counts the number of occupied slots, used + tombstones, which is1401 // This counts the number of occupied slots (not counting tombstones), which is
1399 // what has to stay under the max_load_percentage of capacity.1402 // what has to stay under the max_load_percentage of capacity.
1400 fn load(self: *const Self) Size {1403 fn load(self: *const Self) Size {
1401 const max_load = (self.capacity() * max_load_percentage) / 100;1404 const max_load = (self.capacity() * max_load_percentage) / 100;
...@@ -1587,7 +1590,6 @@ test "std.hash_map ensureUnusedCapacity with tombstones" {...@@ -1587,7 +1590,6 @@ test "std.hash_map ensureUnusedCapacity with tombstones" {
1587 while (i < 100) : (i += 1) {1590 while (i < 100) : (i += 1) {
1588 try map.ensureUnusedCapacity(1);1591 try map.ensureUnusedCapacity(1);
1589 map.putAssumeCapacity(i, i);1592 map.putAssumeCapacity(i, i);
1590 // Remove to create tombstones that still count as load in the hashmap.
1591 _ = map.remove(i);1593 _ = map.remove(i);
1592 }1594 }
1593}1595}
...@@ -1894,6 +1896,38 @@ test "std.hash_map putAssumeCapacity" {...@@ -1894,6 +1896,38 @@ test "std.hash_map putAssumeCapacity" {
1894 try expectEqual(sum, 20);1896 try expectEqual(sum, 20);
1895}1897}
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
1897test "std.hash_map getOrPut" {1931test "std.hash_map getOrPut" {
1898 var map = AutoHashMap(u32, u32).init(std.testing.allocator);1932 var map = AutoHashMap(u32, u32).init(std.testing.allocator);
1899 defer map.deinit();1933 defer map.deinit();