/src/hermes/lib/VM/DictPropertyMap.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) Meta Platforms, Inc. and affiliates. |
3 | | * |
4 | | * This source code is licensed under the MIT license found in the |
5 | | * LICENSE file in the root directory of this source tree. |
6 | | */ |
7 | | |
8 | | #define DEBUG_TYPE "vm" |
9 | | #include "hermes/VM/DictPropertyMap.h" |
10 | | #include "hermes/Support/Statistic.h" |
11 | | #include "hermes/VM/SymbolID-inline.h" |
12 | | |
13 | | HERMES_SLOW_STATISTIC(NumDictLookups, "Number of dictionary lookups"); |
14 | | HERMES_SLOW_STATISTIC(NumExtraHashProbes, "Number of extra hash probes"); |
15 | | |
16 | | namespace hermes { |
17 | | namespace vm { |
18 | | |
19 | | struct DictPropertyMap::detail { |
20 | | /// The upper bound of the search when trying to find the maximum capacity |
21 | | /// of this object, given GC::maxAllocationSize(). |
22 | | /// It was chosen to be a value that is certain to not fit into an allocation; |
23 | | /// at the same time we want to make it smaller, so/ we have arbitrarily |
24 | | /// chosen to divide the max allocation size by two, which is still guaranteed |
25 | | /// not to fit. |
26 | | static constexpr uint32_t kSearchUpperBound = GC::maxAllocationSize() / 2; |
27 | | |
28 | | static_assert( |
29 | | !DictPropertyMap::constWouldFitAllocation(kSearchUpperBound), |
30 | | "kSearchUpperBound should not fit into an allocation"); |
31 | | |
32 | | /// The maximum capacity of DictPropertyMap, given GC::maxAllocationSize(). |
33 | | static constexpr uint32_t kMaxCapacity = |
34 | | DictPropertyMap::constFindMaxCapacity(0, kSearchUpperBound); |
35 | | |
36 | | // Double-check that kMaxCapacity is reasonable. |
37 | | static_assert( |
38 | | DictPropertyMap::constApproxAllocSize64(kMaxCapacity) <= |
39 | | GC::maxAllocationSize(), |
40 | | "invalid kMaxCapacity"); |
41 | | |
42 | | // Ensure that it is safe to double capacities without checking for overflow |
43 | | // until we exceed kMaxCapacity. |
44 | | static_assert( |
45 | | kMaxCapacity < (1u << 31), |
46 | | "kMaxCapacity is unrealistically large"); |
47 | | |
48 | | static_assert( |
49 | | DictPropertyMap::HashPair::canStore(kMaxCapacity), |
50 | | "too few bits to store max possible descriptor index"); |
51 | | }; |
52 | | |
53 | | const VTable DictPropertyMap::vt{CellKind::DictPropertyMapKind, 0}; |
54 | | |
55 | 1 | void DictPropertyMapBuildMeta(const GCCell *cell, Metadata::Builder &mb) { |
56 | 1 | const auto *self = static_cast<const DictPropertyMap *>(cell); |
57 | 1 | mb.setVTable(&DictPropertyMap::vt); |
58 | 1 | mb.addArray( |
59 | 1 | &self->getDescriptorPairs()->first, |
60 | 1 | &self->numDescriptors_, |
61 | 1 | sizeof(DictPropertyMap::DescriptorPair)); |
62 | 1 | } |
63 | | |
64 | 233k | DictPropertyMap::size_type DictPropertyMap::getMaxCapacity() { |
65 | 233k | return detail::kMaxCapacity; |
66 | 233k | } |
67 | | |
68 | | CallResult<PseudoHandle<DictPropertyMap>> DictPropertyMap::create( |
69 | | Runtime &runtime, |
70 | 245k | size_type capacity) { |
71 | 245k | if (LLVM_UNLIKELY(capacity > detail::kMaxCapacity)) { |
72 | 0 | return runtime.raiseRangeError( |
73 | 0 | TwineChar16("Property storage exceeds ") + detail::kMaxCapacity + |
74 | 0 | " properties"); |
75 | 0 | } |
76 | 245k | size_type hashCapacity = calcHashCapacity(capacity); |
77 | 245k | auto *cell = runtime.makeAVariable<DictPropertyMap>( |
78 | 245k | allocationSize(capacity, hashCapacity), capacity, hashCapacity); |
79 | 245k | return createPseudoHandle(cell); |
80 | 245k | } |
81 | | |
82 | | std::pair<bool, DictPropertyMap::HashPair *> DictPropertyMap::lookupEntryFor( |
83 | | DictPropertyMap *self, |
84 | 6.27M | SymbolID symbolID) { |
85 | 6.27M | ++NumDictLookups; |
86 | | |
87 | 6.27M | size_type const mask = self->hashCapacity_ - 1; |
88 | 6.27M | size_type index = hash(symbolID) & mask; |
89 | | |
90 | | // Probing step. |
91 | 6.27M | size_type step = 1; |
92 | | // Save the address of the start of the table to avoid recalculating it. |
93 | 6.27M | HashPair *const tableStart = self->getHashPairs(); |
94 | | // The first deleted entry we found. |
95 | 6.27M | HashPair *deleted = nullptr; |
96 | | |
97 | 6.27M | assert(symbolID.isValid() && "looking for an invalid SymbolID"); |
98 | | |
99 | 12.4M | for (;;) { |
100 | 12.4M | HashPair *curEntry = tableStart + index; |
101 | | |
102 | 12.4M | if (curEntry->isValid()) { |
103 | 8.81M | if (self->isMatch(curEntry, symbolID)) |
104 | 2.67M | return {true, curEntry}; |
105 | 8.81M | } else if (curEntry->isEmpty()) { |
106 | | // If we encountered an empty pair, the search is over - we failed. |
107 | | // Return either this entry or a deleted one, if we encountered one. |
108 | | |
109 | 3.59M | return {false, deleted ? deleted : curEntry}; |
110 | 3.59M | } else { |
111 | 0 | assert(curEntry->isDeleted() && "unexpected HashPair state"); |
112 | | // The first time we encounter a deleted entry, record it so we can |
113 | | // potentially reuse it for insertion. |
114 | 0 | if (!deleted) |
115 | 0 | deleted = curEntry; |
116 | 0 | } |
117 | | |
118 | 6.14M | ++NumExtraHashProbes; |
119 | 6.14M | index = (index + step) & mask; |
120 | 6.14M | ++step; |
121 | 6.14M | } |
122 | 6.27M | } |
123 | | |
124 | | ExecutionStatus DictPropertyMap::grow( |
125 | | MutableHandle<DictPropertyMap> &selfHandleRef, |
126 | | Runtime &runtime, |
127 | 11.7k | size_type newCapacity) { |
128 | 11.7k | auto res = create(runtime, newCapacity); |
129 | 11.7k | if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) { |
130 | 0 | return ExecutionStatus::EXCEPTION; |
131 | 0 | } |
132 | 11.7k | auto *newSelf = res->get(); |
133 | 11.7k | auto *self = *selfHandleRef; |
134 | | |
135 | 11.7k | selfHandleRef = newSelf; |
136 | | |
137 | 11.7k | auto *dst = newSelf->getDescriptorPairs(); |
138 | 11.7k | size_type count = 0; |
139 | | |
140 | 11.7k | for (auto *src = self->getDescriptorPairs(), |
141 | 11.7k | *e = src + self->numDescriptors_.load(std::memory_order_relaxed); |
142 | 369k | src != e; |
143 | 358k | ++src) { |
144 | 358k | if (src->first.isInvalid()) |
145 | 0 | continue; |
146 | | |
147 | 358k | const SymbolID key = src->first; |
148 | | |
149 | | // The new property map doesn't have any valid symbols in it yet, use a |
150 | | // constructor instead of assignment to avoid an invalid write barrier. |
151 | 358k | new (&dst->first) GCSymbolID(key); |
152 | 358k | dst->second = src->second; |
153 | | |
154 | 358k | auto result = lookupEntryFor(newSelf, key); |
155 | 358k | assert(!result.first && "found duplicate entry while growing"); |
156 | 358k | result.second->setDescIndex(count, key); |
157 | | |
158 | 358k | ++dst; |
159 | 358k | ++count; |
160 | 358k | } |
161 | | |
162 | 11.7k | assert( |
163 | 11.7k | count == self->numProperties_ && "numProperties mismatch when growing"); |
164 | | |
165 | 11.7k | newSelf->numProperties_ = count; |
166 | | |
167 | | // Transfer the deleted list to the new instance. |
168 | 11.7k | auto deletedIndex = self->deletedListHead_; |
169 | 11.7k | if (deletedIndex != END_OF_LIST) { |
170 | 0 | newSelf->deletedListHead_ = count; |
171 | 0 | newSelf->deletedListSize_ = self->deletedListSize_; |
172 | |
|
173 | 0 | do { |
174 | 0 | const auto *src = self->getDescriptorPairs() + deletedIndex; |
175 | 0 | assert( |
176 | 0 | src->first == SymbolID::deleted() && |
177 | 0 | "pair in the deleted list is not marked as deleted"); |
178 | | |
179 | | // The new property map doesn't have any valid symbols in it yet, use a |
180 | | // constructor instead of assignment to avoid an invalid write barrier. |
181 | 0 | new (&dst->first) GCSymbolID(SymbolID::deleted()); |
182 | 0 | dst->second.slot = src->second.slot; |
183 | |
|
184 | 0 | deletedIndex = getNextDeletedIndex(src); |
185 | 0 | setNextDeletedIndex( |
186 | 0 | dst, deletedIndex != END_OF_LIST ? count + 1 : END_OF_LIST); |
187 | |
|
188 | 0 | ++dst; |
189 | 0 | ++count; |
190 | 0 | } while (deletedIndex != END_OF_LIST); |
191 | 0 | } |
192 | | |
193 | 11.7k | newSelf->numDescriptors_.store(count, std::memory_order_release); |
194 | 11.7k | assert(count <= newSelf->descriptorCapacity_); |
195 | 11.7k | return ExecutionStatus::RETURNED; |
196 | 11.7k | } |
197 | | |
198 | | CallResult<std::pair<NamedPropertyDescriptor *, bool>> |
199 | | DictPropertyMap::findOrAdd( |
200 | | MutableHandle<DictPropertyMap> &selfHandleRef, |
201 | | Runtime &runtime, |
202 | 544k | SymbolID id) { |
203 | 544k | auto *self = *selfHandleRef; |
204 | 544k | auto numDescriptors = self->numDescriptors_.load(std::memory_order_relaxed); |
205 | 544k | auto found = lookupEntryFor(self, id); |
206 | 544k | if (found.first) { |
207 | 0 | return std::make_pair( |
208 | 0 | &self->getDescriptorPairs()[found.second->getDescIndex()].second, |
209 | 0 | false); |
210 | 0 | } |
211 | | |
212 | | // We want to grow the hash table if the number of occupied hash entries |
213 | | // exceeds 75% of capacity or if the descriptor array is full. Since the |
214 | | // capacity of the table is 4/3 of the capacity of the descriptor array, it is |
215 | | // sufficient to only check for the latter. |
216 | | |
217 | 544k | if (numDescriptors == self->descriptorCapacity_) { |
218 | 11.7k | size_type newCapacity; |
219 | 11.7k | if (self->numProperties_ == self->descriptorCapacity_) { |
220 | | // Double the new capacity, up to kMaxCapacity. However make sure that |
221 | | // we try to allocate at least one extra property. If we are already |
222 | | // exactly at kMaxCapacity, there is nothing we can do, so grow() will |
223 | | // simply fail. |
224 | 11.7k | newCapacity = self->numProperties_ * 2; |
225 | 11.7k | if (newCapacity > detail::kMaxCapacity) |
226 | 0 | newCapacity = |
227 | 0 | std::max(toRValue(detail::kMaxCapacity), self->numProperties_ + 1); |
228 | 11.7k | } else { |
229 | | // Calculate the new capacity to be exactly as much as we need to |
230 | | // accommodate the deleted list plus one extra property. It it happens |
231 | | // to exceed kMaxCapacity, there is nothing we can do, so grow() will |
232 | | // raise an exception. |
233 | 0 | newCapacity = self->numProperties_ + 1 + self->deletedListSize_; |
234 | 0 | } |
235 | | |
236 | 11.7k | if (LLVM_UNLIKELY( |
237 | 11.7k | grow(selfHandleRef, runtime, newCapacity) == |
238 | 11.7k | ExecutionStatus::EXCEPTION)) { |
239 | 0 | return ExecutionStatus::EXCEPTION; |
240 | 0 | } |
241 | | |
242 | 11.7k | self = *selfHandleRef; |
243 | 11.7k | numDescriptors = self->numDescriptors_.load(std::memory_order_relaxed); |
244 | | |
245 | 11.7k | found = lookupEntryFor(self, id); |
246 | 11.7k | } |
247 | | |
248 | 544k | ++self->numProperties_; |
249 | 544k | if (found.second->isDeleted()) |
250 | 0 | self->decDeletedHashCount(); |
251 | | |
252 | 544k | found.second->setDescIndex(numDescriptors, id); |
253 | | |
254 | 544k | auto *descPair = self->getDescriptorPairs() + numDescriptors; |
255 | | |
256 | 544k | descPair->first.set(id, runtime.getHeap()); |
257 | 544k | self->numDescriptors_.fetch_add(1, std::memory_order_acq_rel); |
258 | | |
259 | 544k | return std::make_pair(&descPair->second, true); |
260 | 544k | } |
261 | | |
262 | | void DictPropertyMap::erase( |
263 | | DictPropertyMap *self, |
264 | | Runtime &runtime, |
265 | 0 | PropertyPos pos) { |
266 | 0 | auto *hashPair = self->getHashPairs() + pos.hashPairIndex; |
267 | 0 | auto descIndex = hashPair->getDescIndex(); |
268 | 0 | assert( |
269 | 0 | descIndex < self->numDescriptors_.load(std::memory_order_relaxed) && |
270 | 0 | "descriptor index out of range"); |
271 | | |
272 | 0 | auto *descPair = self->getDescriptorPairs() + descIndex; |
273 | 0 | assert( |
274 | 0 | descPair->first != SymbolID::empty() && |
275 | 0 | "accessing deleted descriptor pair"); |
276 | | |
277 | 0 | hashPair->setDeleted(); |
278 | 0 | descPair->first.set(SymbolID::deleted(), runtime.getHeap()); |
279 | | // Add the descriptor to the deleted list. |
280 | 0 | setNextDeletedIndex(descPair, self->deletedListHead_); |
281 | 0 | self->deletedListHead_ = descIndex; |
282 | 0 | ++self->deletedListSize_; |
283 | |
|
284 | 0 | assert(self->numProperties_ != 0 && "num properties out of sync"); |
285 | 0 | --self->numProperties_; |
286 | 0 | self->incDeletedHashCount(); |
287 | 0 | } |
288 | | |
289 | | SlotIndex DictPropertyMap::allocatePropertySlot( |
290 | | DictPropertyMap *self, |
291 | 208k | Runtime &runtime) { |
292 | | // If there are no deleted properties, the number of properties corresponds |
293 | | // exactly to the number of slots. |
294 | 208k | if (self->deletedListHead_ == END_OF_LIST) |
295 | 208k | return self->numProperties_; |
296 | | |
297 | 0 | auto *deletedPair = self->getDescriptorPairs() + self->deletedListHead_; |
298 | 0 | assert( |
299 | 0 | deletedPair->first == SymbolID::deleted() && |
300 | 0 | "Head of deleted list is not deleted"); |
301 | | |
302 | | // Remove the first element from the deleted list. |
303 | 0 | self->deletedListHead_ = getNextDeletedIndex(deletedPair); |
304 | 0 | --self->deletedListSize_; |
305 | | |
306 | | // Mark the pair as "invalid" instead of "deleted". |
307 | | // No need for symbol write barrier because the previous value was deleted. |
308 | 0 | new (&deletedPair->first) GCSymbolID(SymbolID::empty()); |
309 | |
|
310 | 0 | return deletedPair->second.slot; |
311 | 0 | } |
312 | | |
313 | 0 | void DictPropertyMap::dump() { |
314 | 0 | auto &OS = llvh::errs(); |
315 | |
|
316 | 0 | OS << "DictPropertyMap:" << getDebugAllocationId() << "\n"; |
317 | 0 | OS << " HashPairs[" << hashCapacity_ << "]:\n"; |
318 | 0 | for (unsigned i = 0; i < hashCapacity_; ++i) { |
319 | 0 | auto *pair = getHashPairs() + i; |
320 | 0 | if (pair->isValid()) { |
321 | 0 | OS << " " << pair->getDescIndex() << "\n"; |
322 | 0 | } else if (pair->isEmpty()) { |
323 | 0 | OS << " (empty)\n"; |
324 | 0 | } else { |
325 | 0 | assert(pair->isDeleted()); |
326 | 0 | OS << " (deleted)\n"; |
327 | 0 | } |
328 | 0 | } |
329 | 0 | OS << " Descriptors[" << descriptorCapacity_ << "]:\n"; |
330 | 0 | for (unsigned i = 0; i < descriptorCapacity_; ++i) { |
331 | 0 | auto *pair = getDescriptorPairs() + i; |
332 | 0 | OS << " (" << pair->first << ", " << "(slot=" << pair->second.slot |
333 | 0 | << "))\n"; |
334 | 0 | } |
335 | 0 | } |
336 | | |
337 | | } // namespace vm |
338 | | } // namespace hermes |
339 | | |
340 | | #undef DEBUG_TYPE |