authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-12-17 16:56:43-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-12-17 16:56:43-07:00
log6d04de706ac0fcb20e1468ed6ac7a88deb7e1744
tree3602471fe2456649c8f163f466fb4d01eb8ca435
parent11803a3a569205d640c7ec0b0aedba83f47a6e64

Revert "std: optimize hash_map probe loop condition"

This reverts commit 11803a3a569205d640c7ec0b0aedba83f47a6e64. Observations from the performance dashboard: * strictly worse in terms of CPU instructions * slightly worse wall time (but this can be noisy) * sometimes better, sometimes worse for branch predictions Given that the commit was introducing complexity for optimization's sake, these performance changes do not seem worth it.

1 files changed, 13 insertions(+), 60 deletions(-)

lib/std/hash_map.zig+13-60
......@@ -773,11 +773,6 @@ pub fn HashMapUnmanaged(
773773 self.used = 0;
774774 self.fingerprint = tombstone;
775775 }
776
777 pub fn reset(self: *Metadata) void {
778 self.used = 0;
779 self.fingerprint = free;
780 }
781776 };
782777
783778 comptime {
......@@ -1115,24 +1110,12 @@ pub fn HashMapUnmanaged(
11151110 }
11161111 const mask = self.capacity() - 1;
11171112 const fingerprint = Metadata.takeFingerprint(hash);
1118 var start = @truncate(usize, hash & mask);
1119 var idx = start;
1113 // Don't loop indefinitely when there are no empty slots.
1114 var limit = self.capacity();
1115 var idx = @truncate(usize, hash & mask);
11201116
11211117 var metadata = self.metadata.? + idx;
1122 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
1123 const test_key = &self.keys()[idx];
1124 const eql = ctx.eql(key, test_key.*);
1125 if (eql) return idx;
1126 }
1127 // Temporarily mark the start position as "free" so that the probe loop
1128 // doesn't infinitely loop when all other slots are filled or tombstones.
1129 const saved = metadata[0];
1130 metadata[0].reset();
1131 defer self.metadata.?[start] = saved;
1132 idx = (idx + 1) & mask; // initial idx was already checked
1133 metadata = self.metadata.? + idx;
1134
1135 while (metadata[0].isUsed() or metadata[0].isTombstone()) {
1118 while ((metadata[0].isUsed() or metadata[0].isTombstone()) and limit != 0) {
11361119 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
11371120 const test_key = &self.keys()[idx];
11381121 // If you get a compile error on this line, it means that your generic eql
......@@ -1148,6 +1131,7 @@ pub fn HashMapUnmanaged(
11481131 }
11491132 }
11501133
1134 limit -= 1;
11511135 idx = (idx + 1) & mask;
11521136 metadata = self.metadata.? + idx;
11531137 }
......@@ -1305,39 +1289,12 @@ pub fn HashMapUnmanaged(
13051289 }
13061290 const mask = self.capacity() - 1;
13071291 const fingerprint = Metadata.takeFingerprint(hash);
1308 var start = @truncate(usize, hash & mask);
1309 var idx = start;
1292 var limit = self.capacity();
1293 var idx = @truncate(usize, hash & mask);
13101294
13111295 var first_tombstone_idx: usize = self.capacity(); // invalid index
13121296 var metadata = self.metadata.? + idx;
1313 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
1314 const test_key = &self.keys()[idx];
1315 const eql = ctx.eql(key, test_key.*);
1316 if (eql) {
1317 return GetOrPutResult{
1318 .key_ptr = test_key,
1319 .value_ptr = &self.values()[idx],
1320 .found_existing = true,
1321 };
1322 }
1323 } else if (metadata[0].isTombstone()) {
1324 first_tombstone_idx = idx;
1325 }
1326 // Temporarily mark the start position as "free" so that the probe loop
1327 // doesn't infinitely loop when all other slots are filled or tombstones.
1328 const saved = metadata[0];
1329 metadata[0].reset();
1330 defer {
1331 // Don't restore when the 'home' slot was originally a tombstone
1332 // and the probe missed, since it is now occupied by the new key.
1333 const home = &self.metadata.?[start];
1334 assert(!home.isUsed() or home.fingerprint == fingerprint);
1335 if (!home.isUsed()) home.* = saved;
1336 }
1337 idx = (idx + 1) & mask; // initial idx was already checked
1338 metadata = self.metadata.? + idx;
1339
1340 while (metadata[0].isUsed() or metadata[0].isTombstone()) {
1297 while ((metadata[0].isUsed() or metadata[0].isTombstone()) and limit != 0) {
13411298 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
13421299 const test_key = &self.keys()[idx];
13431300 // If you get a compile error on this line, it means that your generic eql
......@@ -1359,6 +1316,7 @@ pub fn HashMapUnmanaged(
13591316 first_tombstone_idx = idx;
13601317 }
13611318
1319 limit -= 1;
13621320 idx = (idx + 1) & mask;
13631321 metadata = self.metadata.? + idx;
13641322 }
......@@ -1944,7 +1902,6 @@ test "std.hash_map repeat putAssumeCapacity/remove" {
19441902
19451903 try map.ensureTotalCapacity(20);
19461904 const limit = map.unmanaged.available;
1947 const cycles = 100;
19481905
19491906 var i: u32 = 0;
19501907 while (i < limit) : (i += 1) {
......@@ -1954,7 +1911,7 @@ test "std.hash_map repeat putAssumeCapacity/remove" {
19541911 // Repeatedly delete/insert an entry without resizing the map.
19551912 // Put to different keys so entries don't land in the just-freed slot.
19561913 i = 0;
1957 while (i < cycles * limit) : (i += 1) {
1914 while (i < 10 * limit) : (i += 1) {
19581915 try testing.expect(map.remove(i));
19591916 if (i % 2 == 0) {
19601917 map.putAssumeCapacityNoClobber(limit + i, i);
......@@ -1963,13 +1920,9 @@ test "std.hash_map repeat putAssumeCapacity/remove" {
19631920 }
19641921 }
19651922
1966 i = (cycles - 1) * limit;
1967 while (i < cycles * limit) : (i += 1) {
1968 try expectEqual(map.get(i), null); // (removed) key miss
1969 try expectEqual(map.get(limit + i), i); // key hit
1970 const gop = map.getOrPutAssumeCapacity(limit + i);
1971 try testing.expect(gop.found_existing);
1972 try testing.expectEqual(gop.value_ptr.*, i);
1923 i = 9 * limit;
1924 while (i < 10 * limit) : (i += 1) {
1925 try expectEqual(map.get(limit + i), i);
19731926 }
19741927 try expectEqual(map.unmanaged.available, 0);
19751928 try expectEqual(map.unmanaged.count(), limit);