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(...@@ -773,11 +773,6 @@ pub fn HashMapUnmanaged(
773 self.used = 0;773 self.used = 0;
774 self.fingerprint = tombstone;774 self.fingerprint = tombstone;
775 }775 }
776
777 pub fn reset(self: *Metadata) void {
778 self.used = 0;
779 self.fingerprint = free;
780 }
781 };776 };
782777
783 comptime {778 comptime {
...@@ -1115,24 +1110,12 @@ pub fn HashMapUnmanaged(...@@ -1115,24 +1110,12 @@ pub fn HashMapUnmanaged(
1115 }1110 }
1116 const mask = self.capacity() - 1;1111 const mask = self.capacity() - 1;
1117 const fingerprint = Metadata.takeFingerprint(hash);1112 const fingerprint = Metadata.takeFingerprint(hash);
1118 var start = @truncate(usize, hash & mask);1113 // Don't loop indefinitely when there are no empty slots.
1119 var idx = start;1114 var limit = self.capacity();
1115 var idx = @truncate(usize, hash & mask);
11201116
1121 var metadata = self.metadata.? + idx;1117 var metadata = self.metadata.? + idx;
1122 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {1118 while ((metadata[0].isUsed() or metadata[0].isTombstone()) and limit != 0) {
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()) {
1136 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {1119 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
1137 const test_key = &self.keys()[idx];1120 const test_key = &self.keys()[idx];
1138 // 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
...@@ -1148,6 +1131,7 @@ pub fn HashMapUnmanaged(...@@ -1148,6 +1131,7 @@ pub fn HashMapUnmanaged(
1148 }1131 }
1149 }1132 }
11501133
1134 limit -= 1;
1151 idx = (idx + 1) & mask;1135 idx = (idx + 1) & mask;
1152 metadata = self.metadata.? + idx;1136 metadata = self.metadata.? + idx;
1153 }1137 }
...@@ -1305,39 +1289,12 @@ pub fn HashMapUnmanaged(...@@ -1305,39 +1289,12 @@ pub fn HashMapUnmanaged(
1305 }1289 }
1306 const mask = self.capacity() - 1;1290 const mask = self.capacity() - 1;
1307 const fingerprint = Metadata.takeFingerprint(hash);1291 const fingerprint = Metadata.takeFingerprint(hash);
1308 var start = @truncate(usize, hash & mask);1292 var limit = self.capacity();
1309 var idx = start;1293 var idx = @truncate(usize, hash & mask);
13101294
1311 var first_tombstone_idx: usize = self.capacity(); // invalid index1295 var first_tombstone_idx: usize = self.capacity(); // invalid index
1312 var metadata = self.metadata.? + idx;1296 var metadata = self.metadata.? + idx;
1313 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {1297 while ((metadata[0].isUsed() or metadata[0].isTombstone()) and limit != 0) {
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()) {
1341 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {1298 if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) {
1342 const test_key = &self.keys()[idx];1299 const test_key = &self.keys()[idx];
1343 // 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
...@@ -1359,6 +1316,7 @@ pub fn HashMapUnmanaged(...@@ -1359,6 +1316,7 @@ pub fn HashMapUnmanaged(
1359 first_tombstone_idx = idx;1316 first_tombstone_idx = idx;
1360 }1317 }
13611318
1319 limit -= 1;
1362 idx = (idx + 1) & mask;1320 idx = (idx + 1) & mask;
1363 metadata = self.metadata.? + idx;1321 metadata = self.metadata.? + idx;
1364 }1322 }
...@@ -1944,7 +1902,6 @@ test "std.hash_map repeat putAssumeCapacity/remove" {...@@ -1944,7 +1902,6 @@ test "std.hash_map repeat putAssumeCapacity/remove" {
19441902
1945 try map.ensureTotalCapacity(20);1903 try map.ensureTotalCapacity(20);
1946 const limit = map.unmanaged.available;1904 const limit = map.unmanaged.available;
1947 const cycles = 100;
19481905
1949 var i: u32 = 0;1906 var i: u32 = 0;
1950 while (i < limit) : (i += 1) {1907 while (i < limit) : (i += 1) {
...@@ -1954,7 +1911,7 @@ test "std.hash_map repeat putAssumeCapacity/remove" {...@@ -1954,7 +1911,7 @@ test "std.hash_map repeat putAssumeCapacity/remove" {
1954 // Repeatedly delete/insert an entry without resizing the map.1911 // Repeatedly delete/insert an entry without resizing the map.
1955 // Put to different keys so entries don't land in the just-freed slot.1912 // Put to different keys so entries don't land in the just-freed slot.
1956 i = 0;1913 i = 0;
1957 while (i < cycles * limit) : (i += 1) {1914 while (i < 10 * limit) : (i += 1) {
1958 try testing.expect(map.remove(i));1915 try testing.expect(map.remove(i));
1959 if (i % 2 == 0) {1916 if (i % 2 == 0) {
1960 map.putAssumeCapacityNoClobber(limit + i, i);1917 map.putAssumeCapacityNoClobber(limit + i, i);
...@@ -1963,13 +1920,9 @@ test "std.hash_map repeat putAssumeCapacity/remove" {...@@ -1963,13 +1920,9 @@ test "std.hash_map repeat putAssumeCapacity/remove" {
1963 }1920 }
1964 }1921 }
19651922
1966 i = (cycles - 1) * limit;1923 i = 9 * limit;
1967 while (i < cycles * limit) : (i += 1) {1924 while (i < 10 * limit) : (i += 1) {
1968 try expectEqual(map.get(i), null); // (removed) key miss1925 try expectEqual(map.get(limit + i), i);
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);
1973 }1926 }
1974 try expectEqual(map.unmanaged.available, 0);1927 try expectEqual(map.unmanaged.available, 0);
1975 try expectEqual(map.unmanaged.count(), limit);1928 try expectEqual(map.unmanaged.count(), limit);