lib/VM/DictPropertyMap.cpp (233 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#define DEBUG_TYPE "vm"
#include "hermes/VM/DictPropertyMap.h"
#include "hermes/Support/Statistic.h"
#include "hermes/VM/SymbolID-inline.h"
HERMES_SLOW_STATISTIC(NumDictLookups, "Number of dictionary lookups");
HERMES_SLOW_STATISTIC(NumExtraHashProbes, "Number of extra hash probes");
namespace hermes {
namespace vm {
struct DictPropertyMap::detail {
/// The upper bound of the search when trying to find the maximum capacity
/// of this object, given GC::maxAllocationSize().
/// It was chosen to be a value that is certain to not fit into an allocation;
/// at the same time we want to make it smaller, so/ we have arbitrarily
/// chosen to divide the max allocation size by two, which is still guaranteed
/// not to fit.
static constexpr uint32_t kSearchUpperBound = GC::maxAllocationSize() / 2;
static_assert(
!DictPropertyMap::constWouldFitAllocation(kSearchUpperBound),
"kSearchUpperBound should not fit into an allocation");
/// The maximum capacity of DictPropertyMap, given GC::maxAllocationSize().
static constexpr uint32_t kMaxCapacity =
DictPropertyMap::constFindMaxCapacity(0, kSearchUpperBound);
// Double-check that kMaxCapacity is reasonable.
static_assert(
DictPropertyMap::constApproxAllocSize64(kMaxCapacity) <=
GC::maxAllocationSize(),
"invalid kMaxCapacity");
// Ensure that it is safe to double capacities without checking for overflow
// until we exceed kMaxCapacity.
static_assert(
kMaxCapacity < (1u << 31),
"kMaxCapacity is unrealistically large");
static_assert(
DictPropertyMap::HashPair::canStore(kMaxCapacity),
"too few bits to store max possible descriptor index");
};
const VTable DictPropertyMap::vt{CellKind::DictPropertyMapKind, 0};
void DictPropertyMapBuildMeta(const GCCell *cell, Metadata::Builder &mb) {
const auto *self = static_cast<const DictPropertyMap *>(cell);
mb.setVTable(&DictPropertyMap::vt);
mb.addArray(
&self->getDescriptorPairs()->first,
&self->numDescriptors_,
sizeof(DictPropertyMap::DescriptorPair));
}
DictPropertyMap::size_type DictPropertyMap::getMaxCapacity() {
return detail::kMaxCapacity;
}
CallResult<PseudoHandle<DictPropertyMap>> DictPropertyMap::create(
Runtime &runtime,
size_type capacity) {
if (LLVM_UNLIKELY(capacity > detail::kMaxCapacity)) {
return runtime.raiseRangeError(
TwineChar16("Property storage exceeds ") + detail::kMaxCapacity +
" properties");
}
size_type hashCapacity = calcHashCapacity(capacity);
auto *cell = runtime.makeAVariable<DictPropertyMap>(
allocationSize(capacity, hashCapacity), capacity, hashCapacity);
return createPseudoHandle(cell);
}
std::pair<bool, DictPropertyMap::HashPair *> DictPropertyMap::lookupEntryFor(
DictPropertyMap *self,
SymbolID symbolID) {
++NumDictLookups;
size_type const mask = self->hashCapacity_ - 1;
size_type index = hash(symbolID) & mask;
// Probing step.
size_type step = 1;
// Save the address of the start of the table to avoid recalculating it.
HashPair *const tableStart = self->getHashPairs();
// The first deleted entry we found.
HashPair *deleted = nullptr;
assert(symbolID.isValid() && "looking for an invalid SymbolID");
for (;;) {
HashPair *curEntry = tableStart + index;
if (curEntry->isValid()) {
if (self->isMatch(curEntry, symbolID))
return {true, curEntry};
} else if (curEntry->isEmpty()) {
// If we encountered an empty pair, the search is over - we failed.
// Return either this entry or a deleted one, if we encountered one.
return {false, deleted ? deleted : curEntry};
} else {
assert(curEntry->isDeleted() && "unexpected HashPair state");
// The first time we encounter a deleted entry, record it so we can
// potentially reuse it for insertion.
if (!deleted)
deleted = curEntry;
}
++NumExtraHashProbes;
index = (index + step) & mask;
++step;
}
}
ExecutionStatus DictPropertyMap::grow(
MutableHandle<DictPropertyMap> &selfHandleRef,
Runtime &runtime,
size_type newCapacity) {
auto res = create(runtime, newCapacity);
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
auto *newSelf = res->get();
auto *self = *selfHandleRef;
selfHandleRef = newSelf;
auto *dst = newSelf->getDescriptorPairs();
size_type count = 0;
for (auto *src = self->getDescriptorPairs(),
*e = src + self->numDescriptors_.load(std::memory_order_relaxed);
src != e;
++src) {
if (src->first.isInvalid())
continue;
const SymbolID key = src->first;
// The new property map doesn't have any valid symbols in it yet, use a
// constructor instead of assignment to avoid an invalid write barrier.
new (&dst->first) GCSymbolID(key);
dst->second = src->second;
auto result = lookupEntryFor(newSelf, key);
assert(!result.first && "found duplicate entry while growing");
result.second->setDescIndex(count, key);
++dst;
++count;
}
assert(
count == self->numProperties_ && "numProperties mismatch when growing");
newSelf->numProperties_ = count;
// Transfer the deleted list to the new instance.
auto deletedIndex = self->deletedListHead_;
if (deletedIndex != END_OF_LIST) {
newSelf->deletedListHead_ = count;
newSelf->deletedListSize_ = self->deletedListSize_;
do {
const auto *src = self->getDescriptorPairs() + deletedIndex;
assert(
src->first == SymbolID::deleted() &&
"pair in the deleted list is not marked as deleted");
// The new property map doesn't have any valid symbols in it yet, use a
// constructor instead of assignment to avoid an invalid write barrier.
new (&dst->first) GCSymbolID(SymbolID::deleted());
dst->second.slot = src->second.slot;
deletedIndex = getNextDeletedIndex(src);
setNextDeletedIndex(
dst, deletedIndex != END_OF_LIST ? count + 1 : END_OF_LIST);
++dst;
++count;
} while (deletedIndex != END_OF_LIST);
}
newSelf->numDescriptors_.store(count, std::memory_order_release);
assert(count <= newSelf->descriptorCapacity_);
return ExecutionStatus::RETURNED;
}
CallResult<std::pair<NamedPropertyDescriptor *, bool>>
DictPropertyMap::findOrAdd(
MutableHandle<DictPropertyMap> &selfHandleRef,
Runtime &runtime,
SymbolID id) {
auto *self = *selfHandleRef;
auto numDescriptors = self->numDescriptors_.load(std::memory_order_relaxed);
auto found = lookupEntryFor(self, id);
if (found.first) {
return std::make_pair(
&self->getDescriptorPairs()[found.second->getDescIndex()].second,
false);
}
// We want to grow the hash table if the number of occupied hash entries
// exceeds 75% of capacity or if the descriptor array is full. Since the
// capacity of the table is 4/3 of the capacity of the descriptor array, it is
// sufficient to only check for the latter.
if (numDescriptors == self->descriptorCapacity_) {
size_type newCapacity;
if (self->numProperties_ == self->descriptorCapacity_) {
// Double the new capacity, up to kMaxCapacity. However make sure that
// we try to allocate at least one extra property. If we are already
// exactly at kMaxCapacity, there is nothing we can do, so grow() will
// simply fail.
newCapacity = self->numProperties_ * 2;
if (newCapacity > detail::kMaxCapacity)
newCapacity =
std::max(toRValue(detail::kMaxCapacity), self->numProperties_ + 1);
} else {
// Calculate the new capacity to be exactly as much as we need to
// accommodate the deleted list plus one extra property. It it happens
// to exceed kMaxCapacity, there is nothing we can do, so grow() will
// raise an exception.
newCapacity = self->numProperties_ + 1 + self->deletedListSize_;
}
if (LLVM_UNLIKELY(
grow(selfHandleRef, runtime, newCapacity) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
self = *selfHandleRef;
numDescriptors = self->numDescriptors_.load(std::memory_order_relaxed);
found = lookupEntryFor(self, id);
}
++self->numProperties_;
if (found.second->isDeleted())
self->decDeletedHashCount();
found.second->setDescIndex(numDescriptors, id);
auto *descPair = self->getDescriptorPairs() + numDescriptors;
descPair->first.set(id, &runtime.getHeap());
self->numDescriptors_.fetch_add(1, std::memory_order_acq_rel);
return std::make_pair(&descPair->second, true);
}
void DictPropertyMap::erase(
DictPropertyMap *self,
Runtime &runtime,
PropertyPos pos) {
auto *hashPair = self->getHashPairs() + pos.hashPairIndex;
auto descIndex = hashPair->getDescIndex();
assert(
descIndex < self->numDescriptors_.load(std::memory_order_relaxed) &&
"descriptor index out of range");
auto *descPair = self->getDescriptorPairs() + descIndex;
assert(
descPair->first != SymbolID::empty() &&
"accessing deleted descriptor pair");
hashPair->setDeleted();
descPair->first.set(SymbolID::deleted(), &runtime.getHeap());
// Add the descriptor to the deleted list.
setNextDeletedIndex(descPair, self->deletedListHead_);
self->deletedListHead_ = descIndex;
++self->deletedListSize_;
assert(self->numProperties_ != 0 && "num properties out of sync");
--self->numProperties_;
self->incDeletedHashCount();
}
SlotIndex DictPropertyMap::allocatePropertySlot(
DictPropertyMap *self,
Runtime &runtime) {
// If there are no deleted properties, the number of properties corresponds
// exactly to the number of slots.
if (self->deletedListHead_ == END_OF_LIST)
return self->numProperties_;
auto *deletedPair = self->getDescriptorPairs() + self->deletedListHead_;
assert(
deletedPair->first == SymbolID::deleted() &&
"Head of deleted list is not deleted");
// Remove the first element from the deleted list.
self->deletedListHead_ = getNextDeletedIndex(deletedPair);
--self->deletedListSize_;
// Mark the pair as "invalid" instead of "deleted".
// No need for symbol write barrier because the previous value was deleted.
new (&deletedPair->first) GCSymbolID(SymbolID::empty());
return deletedPair->second.slot;
}
void DictPropertyMap::dump() {
auto &OS = llvh::errs();
OS << "DictPropertyMap:" << getDebugAllocationId() << "\n";
OS << " HashPairs[" << hashCapacity_ << "]:\n";
for (unsigned i = 0; i < hashCapacity_; ++i) {
auto *pair = getHashPairs() + i;
if (pair->isValid()) {
OS << " " << pair->getDescIndex() << "\n";
} else if (pair->isEmpty()) {
OS << " (empty)\n";
} else {
assert(pair->isDeleted());
OS << " (deleted)\n";
}
}
OS << " Descriptors[" << descriptorCapacity_ << "]:\n";
for (unsigned i = 0; i < descriptorCapacity_; ++i) {
auto *pair = getDescriptorPairs() + i;
OS << " (" << pair->first << ", "
<< "(slot=" << pair->second.slot << "))\n";
}
}
} // namespace vm
} // namespace hermes
#undef DEBUG_TYPE