authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-07-01 10:14:27+00:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-07-02 04:53:26+00:00
log8b82c40104802c9351b30ef7c5a41e7829ab6d2a
tree8ba2c9c73e5c53db9c902a204ecb179539703dff
parent6f98ef09e31768e3356598ef30e60fe028a0e70c

stage1: reimplement HashMap

The indexes are stored separately using an array of uint8_t, uint16_t, uint32_t, or size_t, depending on the number of entries in the map. Entries only contain a key and a value, no longer have distance_from_start_index or is_used. In theory this should be both faster and use less memory. In practice it seems to have little to no effect. For the standard library tests, vs master branch, the time had no discernable difference, and it shaved off only 13 MiB of peak rss usage.

2 files changed, 199 insertions(+), 131 deletions(-)

src/hash_map.hpp+198-129
......@@ -19,45 +19,64 @@ public:
1919 init_capacity(capacity);
2020 }
2121 void deinit(void) {
22 heap::c_allocator.deallocate(_entries, _capacity);
22 _entries.deinit();
23 heap::c_allocator.deallocate(_index_bytes,
24 _indexes_len * capacity_index_size(_indexes_len));
2325 }
2426
2527 struct Entry {
2628 K key;
2729 V value;
28 bool used;
29 int distance_from_start_index;
3030 };
3131
3232 void clear() {
33 for (int i = 0; i < _capacity; i += 1) {
34 _entries[i].used = false;
35 }
36 _size = 0;
33 _entries.clear();
34 memset(_index_bytes, 0, _indexes_len * capacity_index_size(_indexes_len));
3735 _max_distance_from_start_index = 0;
3836 _modification_count += 1;
3937 }
4038
41 int size() const {
42 return _size;
39 size_t size() const {
40 return _entries.length;
4341 }
4442
4543 void put(const K &key, const V &value) {
4644 _modification_count += 1;
47 internal_put(key, value);
48
49 // if we get too full (60%), double the capacity
50 if (_size * 5 >= _capacity * 3) {
51 Entry *old_entries = _entries;
52 int old_capacity = _capacity;
53 init_capacity(_capacity * 2);
54 // dump all of the old elements into the new table
55 for (int i = 0; i < old_capacity; i += 1) {
56 Entry *old_entry = &old_entries[i];
57 if (old_entry->used)
58 internal_put(old_entry->key, old_entry->value);
45
46 // if we would get too full (60%), double the indexes size
47 if ((_entries.length + 1) * 5 >= _indexes_len * 3) {
48 heap::c_allocator.deallocate(_index_bytes,
49 _indexes_len * capacity_index_size(_indexes_len));
50 _indexes_len *= 2;
51 size_t sz = capacity_index_size(_indexes_len);
52 // This zero initializes the bytes, setting them all empty.
53 _index_bytes = heap::c_allocator.allocate<uint8_t>(_indexes_len * sz);
54 _max_distance_from_start_index = 0;
55 for (size_t i = 0; i < _entries.length; i += 1) {
56 Entry *entry = &_entries.items[i];
57 switch (sz) {
58 case 1:
59 put_index(key_to_index(entry->key), i, (uint8_t*)_index_bytes);
60 continue;
61 case 2:
62 put_index(key_to_index(entry->key), i, (uint16_t*)_index_bytes);
63 continue;
64 case 4:
65 put_index(key_to_index(entry->key), i, (uint32_t*)_index_bytes);
66 continue;
67 default:
68 put_index(key_to_index(entry->key), i, (size_t*)_index_bytes);
69 continue;
70 }
5971 }
60 heap::c_allocator.deallocate(old_entries, old_capacity);
72 }
73
74
75 switch (capacity_index_size(_indexes_len)) {
76 case 1: return internal_put(key, value, (uint8_t*)_index_bytes);
77 case 2: return internal_put(key, value, (uint16_t*)_index_bytes);
78 case 4: return internal_put(key, value, (uint32_t*)_index_bytes);
79 default: return internal_put(key, value, (size_t*)_index_bytes);
6180 }
6281 }
6382
......@@ -81,40 +100,21 @@ public:
81100 return internal_get(key);
82101 }
83102
84 void maybe_remove(const K &key) {
85 if (maybe_get(key)) {
86 remove(key);
87 }
103 bool remove(const K &key) {
104 bool deleted_something = maybe_remove(key);
105 if (!deleted_something)
106 zig_panic("key not found");
107 return deleted_something;
88108 }
89109
90 void remove(const K &key) {
110 bool maybe_remove(const K &key) {
91111 _modification_count += 1;
92 int start_index = key_to_index(key);
93 for (int roll_over = 0; roll_over <= _max_distance_from_start_index; roll_over += 1) {
94 int index = (start_index + roll_over) % _capacity;
95 Entry *entry = &_entries[index];
96
97 if (!entry->used)
98 zig_panic("key not found");
99
100 if (!EqualFn(entry->key, key))
101 continue;
102
103 for (; roll_over < _capacity; roll_over += 1) {
104 int next_index = (start_index + roll_over + 1) % _capacity;
105 Entry *next_entry = &_entries[next_index];
106 if (!next_entry->used || next_entry->distance_from_start_index == 0) {
107 entry->used = false;
108 _size -= 1;
109 return;
110 }
111 *entry = *next_entry;
112 entry->distance_from_start_index -= 1;
113 entry = next_entry;
114 }
115 zig_panic("shifting everything in the table");
112 switch (capacity_index_size(_indexes_len)) {
113 case 1: return internal_remove(key, (uint8_t*)_index_bytes);
114 case 2: return internal_remove(key, (uint16_t*)_index_bytes);
115 case 4: return internal_remove(key, (uint32_t*)_index_bytes);
116 default: return internal_remove(key, (size_t*)_index_bytes);
116117 }
117 zig_panic("key not found");
118118 }
119119
120120 class Iterator {
......@@ -122,24 +122,16 @@ public:
122122 Entry *next() {
123123 if (_inital_modification_count != _table->_modification_count)
124124 zig_panic("concurrent modification");
125 if (_count >= _table->size())
126 return NULL;
127 for (; _index < _table->_capacity; _index += 1) {
128 Entry *entry = &_table->_entries[_index];
129 if (entry->used) {
130 _index += 1;
131 _count += 1;
132 return entry;
133 }
134 }
135 zig_panic("no next item");
125 if (_index >= _table->_entries.length)
126 return nullptr;
127 Entry *entry = &_table->_entries.items[_index];
128 _index += 1;
129 return entry;
136130 }
137131 private:
138132 const HashMap * _table;
139 // how many items have we returned
140 int _count = 0;
141133 // iterator through the entry array
142 int _index = 0;
134 size_t _index = 0;
143135 // used to detect concurrent modification
144136 uint32_t _inital_modification_count;
145137 Iterator(const HashMap * table) :
......@@ -154,89 +146,166 @@ public:
154146 }
155147
156148private:
157
158 Entry *_entries;
159 int _capacity;
160 int _size;
161 int _max_distance_from_start_index;
162 // this is used to detect bugs where a hashtable is edited while an iterator is running.
149 // Maintains insertion order.
150 ZigList<Entry> _entries;
151 // If _indexes_len is less than 2**8, this is an array of uint8_t.
152 // If _indexes_len is less than 2**16, it is an array of uint16_t.
153 // If _indexes_len is less than 2**32, it is an array of uint32_t.
154 // Otherwise it is size_t.
155 // It's off by 1. 0 means empty slot, 1 means index 0, etc.
156 uint8_t *_index_bytes;
157 // This is the number of indexes. When indexes are bytes, it equals number of bytes.
158 // When indexes are uint16_t, _indexes_len is half the number of bytes.
159 size_t _indexes_len;
160
161 size_t _max_distance_from_start_index;
162 // This is used to detect bugs where a hashtable is edited while an iterator is running.
163163 uint32_t _modification_count;
164164
165 void init_capacity(int capacity) {
166 _capacity = capacity;
167 _entries = heap::c_allocator.allocate<Entry>(_capacity);
168 _size = 0;
165 void init_capacity(size_t capacity) {
166 _entries = {};
167 _entries.ensure_capacity(capacity);
168 // So that at capacity it will only be 60% full.
169 _indexes_len = capacity * 5 / 3;
170 size_t sz = capacity_index_size(_indexes_len);
171 // This zero initializes _index_bytes which sets them all to empty.
172 _index_bytes = heap::c_allocator.allocate<uint8_t>(_indexes_len * sz);
173
169174 _max_distance_from_start_index = 0;
170 for (int i = 0; i < _capacity; i += 1) {
171 _entries[i].used = false;
172 }
175 _modification_count = 0;
176 }
177
178 static size_t capacity_index_size(size_t len) {
179 if (len < UINT8_MAX)
180 return 1;
181 if (len < UINT16_MAX)
182 return 2;
183 if (len < UINT32_MAX)
184 return 4;
185 return sizeof(size_t);
173186 }
174187
175 void internal_put(K key, V value) {
176 int start_index = key_to_index(key);
177 for (int roll_over = 0, distance_from_start_index = 0;
178 roll_over < _capacity; roll_over += 1, distance_from_start_index += 1)
188 template <typename I>
189 void internal_put(const K &key, const V &value, I *indexes) {
190 size_t start_index = key_to_index(key);
191 for (size_t roll_over = 0, distance_from_start_index = 0;
192 roll_over < _indexes_len; roll_over += 1, distance_from_start_index += 1)
179193 {
180 int index = (start_index + roll_over) % _capacity;
181 Entry *entry = &_entries[index];
182
183 if (entry->used && !EqualFn(entry->key, key)) {
184 if (entry->distance_from_start_index < distance_from_start_index) {
185 // robin hood to the rescue
186 Entry tmp = *entry;
187 if (distance_from_start_index > _max_distance_from_start_index)
188 _max_distance_from_start_index = distance_from_start_index;
189 *entry = {
190 key,
191 value,
192 true,
193 distance_from_start_index,
194 };
195 key = tmp.key;
196 value = tmp.value;
197 distance_from_start_index = tmp.distance_from_start_index;
198 }
199 continue;
194 size_t index_index = (start_index + roll_over) % _indexes_len;
195 I index_data = indexes[index_index];
196 if (index_data == 0) {
197 _entries.append({key, value});
198 indexes[index_index] = _entries.length;
199 if (distance_from_start_index > _max_distance_from_start_index)
200 _max_distance_from_start_index = distance_from_start_index;
201 return;
200202 }
201
202 if (!entry->used) {
203 // adding an entry. otherwise overwriting old value with
204 // same key
205 _size += 1;
203 Entry *entry = &_entries.items[index_data - 1];
204 if (EqualFn(entry->key, key)) {
205 *entry = {key, value};
206 if (distance_from_start_index > _max_distance_from_start_index)
207 _max_distance_from_start_index = distance_from_start_index;
208 return;
206209 }
207
208 if (distance_from_start_index > _max_distance_from_start_index)
209 _max_distance_from_start_index = distance_from_start_index;
210 *entry = {
211 key,
212 value,
213 true,
214 distance_from_start_index,
215 };
216 return;
217210 }
218 zig_panic("put into a full HashMap");
211 zig_unreachable();
219212 }
220213
214 template <typename I>
215 void put_index(size_t start_index, size_t entry_index, I *indexes) {
216 for (size_t roll_over = 0, distance_from_start_index = 0;
217 roll_over < _indexes_len; roll_over += 1, distance_from_start_index += 1)
218 {
219 size_t index_index = (start_index + roll_over) % _indexes_len;
220 if (indexes[index_index] == 0) {
221 indexes[index_index] = entry_index + 1;
222 if (distance_from_start_index > _max_distance_from_start_index)
223 _max_distance_from_start_index = distance_from_start_index;
224 return;
225 }
226 }
227 zig_unreachable();
228 }
221229
222230 Entry *internal_get(const K &key) const {
223 int start_index = key_to_index(key);
224 for (int roll_over = 0; roll_over <= _max_distance_from_start_index; roll_over += 1) {
225 int index = (start_index + roll_over) % _capacity;
226 Entry *entry = &_entries[index];
231 switch (capacity_index_size(_indexes_len)) {
232 case 1: return internal_get2(key, (uint8_t*)_index_bytes);
233 case 2: return internal_get2(key, (uint16_t*)_index_bytes);
234 case 4: return internal_get2(key, (uint32_t*)_index_bytes);
235 default: return internal_get2(key, (size_t*)_index_bytes);
236 }
237 }
227238
228 if (!entry->used)
229 return NULL;
239 template <typename I>
240 Entry *internal_get2(const K &key, I *indexes) const {
241 size_t start_index = key_to_index(key);
242 for (size_t roll_over = 0; roll_over <= _max_distance_from_start_index; roll_over += 1) {
243 size_t index_index = (start_index + roll_over) % _indexes_len;
244 size_t index_data = indexes[index_index];
245 if (index_data == 0)
246 return nullptr;
230247
248 Entry *entry = &_entries.items[index_data - 1];
231249 if (EqualFn(entry->key, key))
232250 return entry;
233251 }
234 return NULL;
252 return nullptr;
235253 }
236254
237 int key_to_index(const K &key) const {
238 return (int)(HashFunction(key) % ((uint32_t)_capacity));
255 size_t key_to_index(const K &key) const {
256 return ((size_t)HashFunction(key)) % _indexes_len;
257 }
258
259 template <typename I>
260 bool internal_remove(const K &key, I *indexes) {
261 size_t start_index = key_to_index(key);
262 for (size_t roll_over = 0; roll_over <= _max_distance_from_start_index; roll_over += 1) {
263 size_t index_index = (start_index + roll_over) % _indexes_len;
264 size_t index_data = indexes[index_index];
265 if (index_data == 0)
266 return false;
267
268 size_t index = index_data - 1;
269 Entry *entry = &_entries.items[index];
270 if (!EqualFn(entry->key, key))
271 continue;
272
273 indexes[index_index] = 0;
274 _entries.swap_remove(index);
275 if (_entries.length > 0 && _entries.length != index) {
276 // Because of the swap remove, now we need to update the index that was
277 // pointing to the last entry and is now pointing to this removed item slot.
278 update_entry_index(_entries.length, index, indexes);
279 }
280
281 // Now we have to shift over the following indexes.
282 roll_over += 1;
283 for (; roll_over <= _max_distance_from_start_index; roll_over += 1) {
284 size_t next_index = (start_index + roll_over) % _indexes_len;
285 if (indexes[next_index] == 0)
286 break;
287 size_t next_start_index = key_to_index(_entries.items[indexes[next_index]].key);
288 if (next_start_index != start_index)
289 break;
290 indexes[next_index - 1] = indexes[next_index];
291 }
292
293 return true;
294 }
295 return false;
239296 }
240};
241297
298 template <typename I>
299 void update_entry_index(size_t old_entry_index, size_t new_entry_index, I *indexes) {
300 size_t start_index = key_to_index(_entries.items[new_entry_index].key);
301 for (size_t roll_over = 0; roll_over <= _max_distance_from_start_index; roll_over += 1) {
302 size_t index_index = (start_index + roll_over) % _indexes_len;
303 if (indexes[index_index] == old_entry_index + 1) {
304 indexes[index_index] = new_entry_index + 1;
305 return;
306 }
307 }
308 zig_unreachable();
309 }
310};
242311#endif
test/stage1/behavior/type_info.zig+1-2
......@@ -251,14 +251,13 @@ fn testStruct() void {
251251}
252252
253253const TestStruct = packed struct {
254 const Self = @This();
255
256254 fieldA: usize,
257255 fieldB: void,
258256 fieldC: *Self,
259257 fieldD: u32 = 4,
260258
261259 pub fn foo(self: *const Self) void {}
260 const Self = @This();
262261};
263262
264263test "type info: function type info" {