[vm] Use a hash map for larger type argument instantiation caches.
Previously, the VM used a linear array to cache previous instantiations of a type arguments object. Now once the cache hits a certain number of occupied entries, the VM changes to using a hash-based approach. The InstantiateTypeArguments stubs have not yet been updated to traverse the hash-based cache, so once the cache has grown too large, all attempts at instantiations, even those that are in the cache, go to the runtime. Thus, until the stubs are updated, this is only an improvement if the cost of traversing the linear cache dominates the cost of making a runtime call. Our benchmarks see a ~40% performance regression for hash-based caches of size 100 but a ~400% performance improvement for hash-based caches of size 1000. Thus, we currently split the difference and set the maximum size of linear caches to 500. TEST=vm/cc/TypeArguments_Cache_ManyInstantiations Bug: https://github.com/dart-lang/sdk/issues/48344 Change-Id: I7f1376943523bb5bcd8b175cfb1936779ea73d60 Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-dwarf-linux-product-x64-try,vm-kernel-precomp-linux-product-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-kernel-precomp-nnbd-mac-release-arm64-try,vm-kernel-precomp-nnbd-linux-release-simarm_x64-try,vm-kernel-precomp-linux-release-simarm-try,vm-kernel-precomp-nnbd-linux-release-x64-try,vm-kernel-precomp-nnbd-linux-release-simarm64-try,vm-kernel-precomp-nnbd-linux-debug-simriscv64-try,vm-kernel-precomp-tsan-linux-release-x64-try,vm-kernel-tsan-linux-release-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/265325 Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Ryan Macnak <rmacnak@google.com> Commit-Queue: Tess Strickland <sstrickl@google.com>
This commit is contained in:
committed by
Commit Queue
parent
333d924504
commit
4f925105cf
@@ -14,21 +14,6 @@
|
||||
|
||||
namespace dart {
|
||||
|
||||
// Implementation is from "Hacker's Delight" by Henry S. Warren, Jr.,
|
||||
// figure 3-3, page 48, where the function is called clp2.
|
||||
uintptr_t Utils::RoundUpToPowerOfTwo(uintptr_t x) {
|
||||
x = x - 1;
|
||||
x = x | (x >> 1);
|
||||
x = x | (x >> 2);
|
||||
x = x | (x >> 4);
|
||||
x = x | (x >> 8);
|
||||
x = x | (x >> 16);
|
||||
#if defined(ARCH_IS_64_BIT)
|
||||
x = x | (x >> 32);
|
||||
#endif // defined(ARCH_IS_64_BIT)
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
int Utils::CountLeadingZeros64(uint64_t x) {
|
||||
#if defined(ARCH_IS_32_BIT)
|
||||
const uint32_t x_hi = static_cast<uint32_t>(x >> 32);
|
||||
|
||||
@@ -114,7 +114,20 @@ class Utils {
|
||||
RoundUp(reinterpret_cast<uword>(x), alignment, offset));
|
||||
}
|
||||
|
||||
static uintptr_t RoundUpToPowerOfTwo(uintptr_t x);
|
||||
// Implementation is from "Hacker's Delight" by Henry S. Warren, Jr.,
|
||||
// figure 3-3, page 48, where the function is called clp2.
|
||||
static constexpr uintptr_t RoundUpToPowerOfTwo(uintptr_t x) {
|
||||
x = x - 1;
|
||||
x = x | (x >> 1);
|
||||
x = x | (x >> 2);
|
||||
x = x | (x >> 4);
|
||||
x = x | (x >> 8);
|
||||
x = x | (x >> 16);
|
||||
#if defined(ARCH_IS_64_BIT)
|
||||
x = x | (x >> 32);
|
||||
#endif // defined(ARCH_IS_64_BIT)
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
static constexpr int CountOneBits64(uint64_t x) {
|
||||
// Apparently there are x64 chips without popcount.
|
||||
|
||||
@@ -6052,7 +6052,8 @@ class VMSerializationRoots : public SerializationRoots {
|
||||
s->AddBaseObject(Object::transition_sentinel().ptr(), "Null",
|
||||
"transition_sentinel");
|
||||
s->AddBaseObject(Object::empty_array().ptr(), "Array", "<empty_array>");
|
||||
s->AddBaseObject(Object::zero_array().ptr(), "Array", "<zero_array>");
|
||||
s->AddBaseObject(Object::empty_instantiations_cache_array().ptr(), "Array",
|
||||
"<empty_instantiations_cache_array>");
|
||||
s->AddBaseObject(Object::dynamic_type().ptr(), "Type", "<dynamic type>");
|
||||
s->AddBaseObject(Object::void_type().ptr(), "Type", "<void type>");
|
||||
s->AddBaseObject(Object::empty_type_arguments().ptr(), "TypeArguments",
|
||||
@@ -6174,7 +6175,7 @@ class VMDeserializationRoots : public DeserializationRoots {
|
||||
d->AddBaseObject(Object::sentinel().ptr());
|
||||
d->AddBaseObject(Object::transition_sentinel().ptr());
|
||||
d->AddBaseObject(Object::empty_array().ptr());
|
||||
d->AddBaseObject(Object::zero_array().ptr());
|
||||
d->AddBaseObject(Object::empty_instantiations_cache_array().ptr());
|
||||
d->AddBaseObject(Object::dynamic_type().ptr());
|
||||
d->AddBaseObject(Object::void_type().ptr());
|
||||
d->AddBaseObject(Object::empty_type_arguments().ptr());
|
||||
|
||||
@@ -284,42 +284,58 @@ void StubCodeCompiler::GenerateInstantiateTypeArgumentsStub(
|
||||
const Register kEntryReg = InstantiationABI::kResultTypeArgumentsReg;
|
||||
// Lookup cache before calling runtime.
|
||||
__ LoadCompressed(
|
||||
kEntryReg,
|
||||
InstantiationABI::kScratchReg,
|
||||
compiler::FieldAddress(InstantiationABI::kUninstantiatedTypeArgumentsReg,
|
||||
target::TypeArguments::instantiations_offset()));
|
||||
__ LoadFieldAddressForOffset(kEntryReg, kEntryReg, Array::data_offset());
|
||||
// Both the linear and hash-based cache access loops assume kEntryReg is
|
||||
// the address of the first cache entry, so set it before branching.
|
||||
__ LoadFieldAddressForOffset(kEntryReg, InstantiationABI::kScratchReg,
|
||||
Array::data_offset());
|
||||
__ AddImmediate(kEntryReg, TypeArguments::Cache::kHeaderSize *
|
||||
target::kCompressedWordSize);
|
||||
|
||||
// The instantiations cache is initialized with Object::zero_array() and is
|
||||
// therefore guaranteed to contain kNoInstantiator. No length check needed.
|
||||
compiler::Label loop, next, found, call_runtime;
|
||||
__ Bind(&loop);
|
||||
compiler::Label linear_cache_loop, hash_cache_loop, found, call_runtime;
|
||||
|
||||
// Use load-acquire to test for sentinel, if we found non-sentinel it is safe
|
||||
// to access the other entries. If we found a sentinel we go to runtime.
|
||||
__ LoadAcquireCompressed(
|
||||
InstantiationABI::kScratchReg, kEntryReg,
|
||||
TypeArguments::Instantiation::kInstantiatorTypeArgsIndex *
|
||||
target::kCompressedWordSize);
|
||||
__ CompareImmediate(InstantiationABI::kScratchReg,
|
||||
Smi::RawValue(TypeArguments::kNoInstantiator),
|
||||
kObjectBytes);
|
||||
__ BranchIf(EQUAL, &call_runtime, compiler::Assembler::kNearJump);
|
||||
// There is a maximum size for linear caches that is smaller than the size of
|
||||
// any hash-based cache, so we check the size of the backing array to
|
||||
// determine if this is a linear or hash-based cache.
|
||||
__ LoadFromSlot(InstantiationABI::kScratchReg, InstantiationABI::kScratchReg,
|
||||
Slot::Array_length());
|
||||
__ CompareImmediate(
|
||||
InstantiationABI::kScratchReg,
|
||||
target::ToRawSmi(TypeArguments::Cache::kMaxLinearCacheSize));
|
||||
__ BranchIf(GREATER, &call_runtime);
|
||||
|
||||
__ Bind(&linear_cache_loop);
|
||||
// Use load-acquire to get the entry.
|
||||
static_assert(TypeArguments::Cache::kSentinelIndex ==
|
||||
TypeArguments::Cache::kInstantiatorTypeArgsIndex,
|
||||
"sentinel is not same index as instantiator type args");
|
||||
__ LoadAcquireCompressed(InstantiationABI::kScratchReg, kEntryReg,
|
||||
TypeArguments::Cache::kInstantiatorTypeArgsIndex *
|
||||
target::kCompressedWordSize);
|
||||
// Must either be the sentinel (a Smi) or a TypeArguments object, so test for
|
||||
// a Smi and go to the runtime if found.
|
||||
__ BranchIfSmi(InstantiationABI::kScratchReg, &call_runtime,
|
||||
compiler::Assembler::kNearJump);
|
||||
// We have a TypeArguments object, so this is an array cache and we can
|
||||
// safely access the other entries.
|
||||
compiler::Label next;
|
||||
__ CompareRegisters(InstantiationABI::kScratchReg,
|
||||
InstantiationABI::kInstantiatorTypeArgumentsReg);
|
||||
__ BranchIf(NOT_EQUAL, &next, compiler::Assembler::kNearJump);
|
||||
__ LoadCompressed(
|
||||
InstantiationABI::kScratchReg,
|
||||
compiler::Address(kEntryReg,
|
||||
TypeArguments::Instantiation::kFunctionTypeArgsIndex *
|
||||
TypeArguments::Cache::kFunctionTypeArgsIndex *
|
||||
target::kCompressedWordSize));
|
||||
__ CompareRegisters(InstantiationABI::kScratchReg,
|
||||
InstantiationABI::kFunctionTypeArgumentsReg);
|
||||
__ BranchIf(EQUAL, &found, compiler::Assembler::kNearJump);
|
||||
__ Bind(&next);
|
||||
__ AddImmediate(kEntryReg, TypeArguments::Instantiation::kSizeInWords *
|
||||
__ AddImmediate(kEntryReg, TypeArguments::Cache::kEntrySize *
|
||||
target::kCompressedWordSize);
|
||||
__ Jump(&loop, compiler::Assembler::kNearJump);
|
||||
__ Jump(&linear_cache_loop, compiler::Assembler::kNearJump);
|
||||
|
||||
// Instantiate non-null type arguments.
|
||||
// A runtime call to instantiate the type arguments is required.
|
||||
@@ -352,9 +368,9 @@ void StubCodeCompiler::GenerateInstantiateTypeArgumentsStub(
|
||||
__ Bind(&found);
|
||||
__ LoadCompressed(
|
||||
InstantiationABI::kResultTypeArgumentsReg,
|
||||
compiler::Address(
|
||||
kEntryReg, TypeArguments::Instantiation::kInstantiatedTypeArgsIndex *
|
||||
target::kCompressedWordSize));
|
||||
compiler::Address(kEntryReg,
|
||||
TypeArguments::Cache::kInstantiatedTypeArgsIndex *
|
||||
target::kCompressedWordSize));
|
||||
__ Ret();
|
||||
}
|
||||
|
||||
|
||||
+324
-91
@@ -743,11 +743,11 @@ void Object::Init(IsolateGroup* isolate_group) {
|
||||
*bool_true_ = true_;
|
||||
*bool_false_ = false_;
|
||||
|
||||
// Initialize the empty and zero array handles to null_ in order to be able to
|
||||
// check if the empty and zero arrays were allocated (RAW_NULL is not
|
||||
// available).
|
||||
// Initialize the empty array and empty instantiations cache array handles to
|
||||
// null_ in order to be able to check if the empty and zero arrays were
|
||||
// allocated (RAW_NULL is not available).
|
||||
*empty_array_ = Array::null();
|
||||
*zero_array_ = Array::null();
|
||||
*empty_instantiations_cache_array_ = Array::null();
|
||||
|
||||
Class& cls = Class::Handle();
|
||||
|
||||
@@ -1001,17 +1001,32 @@ void Object::Init(IsolateGroup* isolate_group) {
|
||||
}
|
||||
|
||||
Smi& smi = Smi::Handle();
|
||||
// Allocate and initialize the zero_array instance.
|
||||
// Allocate and initialize the empty instantiations cache array instance,
|
||||
// which contains metadata as the first element and a sentinel value
|
||||
// at the start of the first entry.
|
||||
{
|
||||
uword address = heap->Allocate(thread, Array::InstanceSize(1), Heap::kOld);
|
||||
InitializeObject(address, kImmutableArrayCid, Array::InstanceSize(1),
|
||||
const intptr_t array_size =
|
||||
TypeArguments::Cache::kHeaderSize + TypeArguments::Cache::kEntrySize;
|
||||
uword address =
|
||||
heap->Allocate(thread, Array::InstanceSize(array_size), Heap::kOld);
|
||||
InitializeObject(address, kImmutableArrayCid,
|
||||
Array::InstanceSize(array_size),
|
||||
Array::ContainsCompressedPointers());
|
||||
Array::initializeHandle(zero_array_,
|
||||
Array::initializeHandle(empty_instantiations_cache_array_,
|
||||
static_cast<ArrayPtr>(address + kHeapObjectTag));
|
||||
zero_array_->untag()->set_length(Smi::New(1));
|
||||
empty_instantiations_cache_array_->untag()->set_length(
|
||||
Smi::New(array_size));
|
||||
// The empty cache has no occupied entries.
|
||||
smi = Smi::New(0);
|
||||
zero_array_->SetAt(0, smi);
|
||||
zero_array_->SetCanonical();
|
||||
empty_instantiations_cache_array_->SetAt(
|
||||
TypeArguments::Cache::kOccupiedEntriesIndex, smi);
|
||||
// Make the first (and only) entry unoccupied by setting its first element
|
||||
// to the sentinel value.
|
||||
smi = TypeArguments::Cache::Sentinel();
|
||||
InstantiationsCacheTable table(*empty_instantiations_cache_array_);
|
||||
table.At(0).Set<TypeArguments::Cache::kSentinelIndex>(smi);
|
||||
// The other contents of the array are immaterial.
|
||||
empty_instantiations_cache_array_->SetCanonical();
|
||||
}
|
||||
|
||||
// Allocate and initialize the canonical empty context scope object.
|
||||
@@ -1257,8 +1272,8 @@ void Object::Init(IsolateGroup* isolate_group) {
|
||||
ASSERT(null_compressed_stackmaps_->IsCompressedStackMaps());
|
||||
ASSERT(!empty_array_->IsSmi());
|
||||
ASSERT(empty_array_->IsArray());
|
||||
ASSERT(!zero_array_->IsSmi());
|
||||
ASSERT(zero_array_->IsArray());
|
||||
ASSERT(!empty_instantiations_cache_array_->IsSmi());
|
||||
ASSERT(empty_instantiations_cache_array_->IsArray());
|
||||
ASSERT(!empty_type_arguments_->IsSmi());
|
||||
ASSERT(empty_type_arguments_->IsTypeArguments());
|
||||
ASSERT(!empty_context_scope_->IsSmi());
|
||||
@@ -6739,23 +6754,294 @@ bool TypeArguments::IsDynamicTypes(bool raw_instantiated,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TypeArguments::HasInstantiations() const {
|
||||
const Array& prior_instantiations = Array::Handle(instantiations());
|
||||
ASSERT(prior_instantiations.Length() > 0); // Always at least a sentinel.
|
||||
return prior_instantiations.Length() > 1;
|
||||
TypeArguments::Cache::Cache(Zone* zone, const TypeArguments& source)
|
||||
: zone_(ASSERT_NOTNULL(zone)),
|
||||
cache_container_(&source),
|
||||
data_(Array::Handle(source.instantiations())),
|
||||
smi_handle_(Smi::Handle(zone)) {
|
||||
ASSERT(IsolateGroup::Current()
|
||||
->type_arguments_canonicalization_mutex()
|
||||
->IsOwnedByCurrentThread());
|
||||
}
|
||||
|
||||
intptr_t TypeArguments::NumInstantiations() const {
|
||||
const Array& prior_instantiations = Array::Handle(instantiations());
|
||||
ASSERT(prior_instantiations.Length() > 0); // Always at least a sentinel.
|
||||
intptr_t num = 0;
|
||||
intptr_t i = 0;
|
||||
while (prior_instantiations.At(i) !=
|
||||
Smi::New(TypeArguments::kNoInstantiator)) {
|
||||
i += TypeArguments::Instantiation::kSizeInWords;
|
||||
num++;
|
||||
TypeArguments::Cache::Cache(Zone* zone, const Array& array)
|
||||
: zone_(ASSERT_NOTNULL(zone)),
|
||||
cache_container_(nullptr),
|
||||
data_(Array::Handle(array.ptr())),
|
||||
smi_handle_(Smi::Handle(zone)) {
|
||||
ASSERT(IsolateGroup::Current()
|
||||
->type_arguments_canonicalization_mutex()
|
||||
->IsOwnedByCurrentThread());
|
||||
}
|
||||
|
||||
bool TypeArguments::Cache::IsHash(const Array& array) {
|
||||
return array.Length() > kMaxLinearCacheSize;
|
||||
}
|
||||
|
||||
intptr_t TypeArguments::Cache::NumOccupied(const Array& array) {
|
||||
return RawSmiValue(Smi::RawCast(array.AtAcquire(kOccupiedEntriesIndex)));
|
||||
}
|
||||
|
||||
#if defined(DEBUG)
|
||||
bool TypeArguments::Cache::IsValidStorageLocked(const Array& array) {
|
||||
// We only require the mutex be held so we don't need to use acquire/release
|
||||
// semantics to access and set the number of occupied entries in the header.
|
||||
ASSERT(IsolateGroup::Current()
|
||||
->type_arguments_canonicalization_mutex()
|
||||
->IsOwnedByCurrentThread());
|
||||
// Quick check against the empty linear cache.
|
||||
if (array.ptr() == EmptyStorage().ptr()) return true;
|
||||
const intptr_t num_occupied = NumOccupied(array);
|
||||
// We should be using the same shared value for an empty cache.
|
||||
if (num_occupied == 0) return false;
|
||||
const intptr_t storage_len = array.Length();
|
||||
// All caches have the metadata followed by a series of entries.
|
||||
if ((storage_len % kEntrySize) != kHeaderSize) return false;
|
||||
const intptr_t num_entries = NumEntries(array);
|
||||
// Linear caches contain at least one unoccupied entry, and hash-based caches
|
||||
// grow prior to hitting 100% occupancy.
|
||||
if (num_occupied >= num_entries) return false;
|
||||
// In a linear cache, all entries with indexes smaller than [num_occupied]
|
||||
// should be occupied and ones greater than or equal should be unoccupied.
|
||||
const bool is_linear_cache = IsLinear(array);
|
||||
// The capacity of a hash-based cache must be a power of two (see
|
||||
// EnsureCapacityLocked as to why).
|
||||
if (!is_linear_cache && !Utils::IsPowerOfTwo(num_entries)) return false;
|
||||
for (intptr_t i = 0; i < num_entries; i++) {
|
||||
const intptr_t index = kHeaderSize + i * kEntrySize;
|
||||
if (array.At(index + kSentinelIndex) == Sentinel()) {
|
||||
if (is_linear_cache && i < num_occupied) return false;
|
||||
continue;
|
||||
}
|
||||
if (is_linear_cache && i >= num_occupied) return false;
|
||||
// The elements of an occupied entry are all TypeArguments values.
|
||||
for (intptr_t j = index; j < index + kEntrySize; j++) {
|
||||
if (!array.At(j)->IsHeapObject()) return false;
|
||||
if (array.At(j) == Object::null()) continue; // null is a valid TAV.
|
||||
if (!array.At(j)->IsTypeArguments()) return false;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool TypeArguments::Cache::IsOccupied(intptr_t entry) const {
|
||||
InstantiationsCacheTable table(data_);
|
||||
ASSERT(entry >= 0 && entry < table.Length());
|
||||
return table.At(entry).Get<kSentinelIndex>() != Sentinel();
|
||||
}
|
||||
|
||||
TypeArgumentsPtr TypeArguments::Cache::Retrieve(intptr_t entry) const {
|
||||
ASSERT(IsOccupied(entry));
|
||||
InstantiationsCacheTable table(data_);
|
||||
return table.At(entry).Get<kInstantiatedTypeArgsIndex>();
|
||||
}
|
||||
|
||||
intptr_t TypeArguments::Cache::NumEntries(const Array& array) {
|
||||
InstantiationsCacheTable table(array);
|
||||
return table.Length();
|
||||
}
|
||||
|
||||
TypeArguments::Cache::KeyLocation TypeArguments::Cache::FindKeyOrUnused(
|
||||
const Array& array,
|
||||
const TypeArguments& instantiator_tav,
|
||||
const TypeArguments& function_tav) {
|
||||
const bool is_hash = IsHash(array);
|
||||
InstantiationsCacheTable table(array);
|
||||
const intptr_t num_entries = table.Length();
|
||||
// For a linear cache, start at the first entry and probe linearly. This can
|
||||
// be done because a linear cache always has at least one unoccupied entry
|
||||
// after all the occupied ones.
|
||||
intptr_t probe = 0;
|
||||
intptr_t probe_distance = 1;
|
||||
if (is_hash) {
|
||||
// For a hash-based cache, instead start at an entry determined by the hash
|
||||
// of the keys.
|
||||
auto hash = FinalizeHash(
|
||||
CombineHashes(instantiator_tav.Hash(), function_tav.Hash()));
|
||||
probe = hash & (num_entries - 1);
|
||||
}
|
||||
while (true) {
|
||||
const auto& tuple = table.At(probe);
|
||||
if (tuple.Get<kSentinelIndex>() == Sentinel()) break;
|
||||
if ((tuple.Get<kInstantiatorTypeArgsIndex>() == instantiator_tav.ptr()) &&
|
||||
(tuple.Get<kFunctionTypeArgsIndex>() == function_tav.ptr())) {
|
||||
return {probe, true};
|
||||
}
|
||||
// Advance probe by the current probing distance.
|
||||
probe = probe + probe_distance;
|
||||
if (is_hash) {
|
||||
// Wrap around if the probe goes off the end of the entries array.
|
||||
probe = probe & (num_entries - 1);
|
||||
// We had a collision, so increase the probe distance. See comment in
|
||||
// EnsureCapacityLocked for an explanation of how this hits all slots.
|
||||
probe_distance++;
|
||||
}
|
||||
}
|
||||
// We should always get the next slot for a linear cache.
|
||||
ASSERT(is_hash || probe == NumOccupied(array));
|
||||
return {probe, false};
|
||||
}
|
||||
|
||||
TypeArguments::Cache::KeyLocation TypeArguments::Cache::AddEntry(
|
||||
intptr_t entry,
|
||||
const TypeArguments& instantiator_tav,
|
||||
const TypeArguments& function_tav,
|
||||
const TypeArguments& instantiated_tav) const {
|
||||
// We don't do mutating operations in tests without a TypeArguments object.
|
||||
ASSERT(cache_container_ != nullptr);
|
||||
#if defined(DEBUG)
|
||||
auto loc = FindKeyOrUnused(instantiator_tav, function_tav);
|
||||
ASSERT_EQUAL(loc.entry, entry);
|
||||
ASSERT(!loc.present);
|
||||
#endif
|
||||
// Double-check we got the expected entry index when adding to a linear array.
|
||||
ASSERT(!IsLinear() || entry == NumOccupied());
|
||||
const intptr_t new_occupied = NumOccupied() + 1;
|
||||
const bool storage_changed = EnsureCapacity(new_occupied);
|
||||
// Note that this call to IsLinear() may return a different result than the
|
||||
// earlier, since EnsureCapacity() may have swapped to hash-based storage.
|
||||
if (storage_changed && !IsLinear()) {
|
||||
// The capacity of the array has changed, and the capacity is used when
|
||||
// probing further into the array due to collisions. Thus, we need to redo
|
||||
// the entry index calculation.
|
||||
auto loc = FindKeyOrUnused(instantiator_tav, function_tav);
|
||||
ASSERT(!loc.present);
|
||||
entry = loc.entry;
|
||||
}
|
||||
|
||||
// Increment the number of occupied entries prior to adding the entry.
|
||||
// Only the Cache class uses the information, and Cache objects are only
|
||||
// created when holding the type arguments canonicalization mutex, so we
|
||||
// don't need a store-release barrier for this.
|
||||
smi_handle_ = Smi::New(new_occupied);
|
||||
data_.SetAt(kOccupiedEntriesIndex, smi_handle_);
|
||||
|
||||
InstantiationsCacheTable table(data_);
|
||||
const auto& tuple = table.At(entry);
|
||||
// The parts of the tuple that aren't used for sentinel checking are only
|
||||
// retrieved if the entry is occupied. Entries in the cache are never deleted,
|
||||
// so once the entry is marked as occupied, the contents of that entry never
|
||||
// change. Thus, we don't need store-release barriers here.
|
||||
tuple.Set<kFunctionTypeArgsIndex>(function_tav);
|
||||
tuple.Set<kInstantiatedTypeArgsIndex>(instantiated_tav);
|
||||
// For the sentinel position, though, we do.
|
||||
static_assert(
|
||||
kSentinelIndex == kInstantiatorTypeArgsIndex,
|
||||
"the sentinel position is not protected with a store-release barrier");
|
||||
tuple.Set<kInstantiatorTypeArgsIndex, std::memory_order_release>(
|
||||
instantiator_tav);
|
||||
|
||||
if (storage_changed) {
|
||||
// Only check for validity on growth, just to keep the overhead on DEBUG
|
||||
// builds down.
|
||||
DEBUG_ASSERT(IsValidStorageLocked(data_));
|
||||
// Update the container of the original cache to point to the new one.
|
||||
cache_container_->set_instantiations(data_);
|
||||
}
|
||||
|
||||
return {entry, true};
|
||||
}
|
||||
|
||||
SmiPtr TypeArguments::Cache::Sentinel() {
|
||||
return Smi::New(kSentinelValue);
|
||||
}
|
||||
|
||||
bool TypeArguments::Cache::EnsureCapacity(intptr_t new_occupied) const {
|
||||
ASSERT(new_occupied > NumOccupied());
|
||||
// How many entries are in the current array (including unoccupied entries).
|
||||
const intptr_t current_capacity = NumEntries();
|
||||
|
||||
// Early returns for cases where no growth is needed.
|
||||
const bool is_linear = IsLinear();
|
||||
if (is_linear) {
|
||||
// We need at least one unoccupied entry in addition to the occupied ones.
|
||||
if (current_capacity > new_occupied) return false;
|
||||
} else {
|
||||
if (LoadFactor(new_occupied, current_capacity) < kMaxLoadFactor) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (new_occupied <= kMaxLinearCacheEntries) {
|
||||
ASSERT(is_linear);
|
||||
// Not enough room for both the new entry and at least one unoccupied
|
||||
// entry, so grow the tuple capacity of the linear cache by about 50%,
|
||||
// ensuring that space for at least one new tuple is added, capping the
|
||||
// total number of occupied entries to the max allowed.
|
||||
const intptr_t new_capacity =
|
||||
Utils::Minimum(current_capacity + (current_capacity >> 1),
|
||||
kMaxLinearCacheEntries) +
|
||||
1;
|
||||
const intptr_t cache_size = kHeaderSize + new_capacity * kEntrySize;
|
||||
ASSERT(cache_size <= kMaxLinearCacheSize);
|
||||
data_ = Array::Grow(data_, cache_size, Heap::kOld);
|
||||
ASSERT(!data_.IsNull());
|
||||
// No need to adjust the number of occupied entries or old entries, as they
|
||||
// are copied over by Array::Grow. Just mark any new entries as unoccupied.
|
||||
smi_handle_ = Sentinel();
|
||||
InstantiationsCacheTable table(data_);
|
||||
for (intptr_t i = current_capacity; i < new_capacity; i++) {
|
||||
const auto& tuple = table.At(i);
|
||||
tuple.Set<kSentinelIndex>(smi_handle_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Either we're converting a linear cache into a hash-based cache, or the
|
||||
// load factor of the hash-based cache has increased to the point where we
|
||||
// need to grow it.
|
||||
const intptr_t new_capacity =
|
||||
is_linear ? kNumInitialHashCacheEntries : 2 * current_capacity;
|
||||
// Because we use quadratic (actually triangle number) probing it is
|
||||
// important that the size is a power of two (otherwise we could fail to
|
||||
// find an empty slot). This is described in Knuth's The Art of Computer
|
||||
// Programming Volume 2, Chapter 6.4, exercise 20 (solution in the
|
||||
// appendix, 2nd edition).
|
||||
ASSERT(Utils::IsPowerOfTwo(new_capacity));
|
||||
ASSERT(LoadFactor(new_occupied, new_capacity) < kMaxLoadFactor);
|
||||
const intptr_t new_size = kHeaderSize + new_capacity * kEntrySize;
|
||||
const auto& new_data =
|
||||
Array::Handle(zone_, Array::NewUninitialized(new_size, Heap::kOld));
|
||||
ASSERT(!new_data.IsNull());
|
||||
// First copy over the metadata.
|
||||
auto& object = Object::Handle(zone_);
|
||||
for (intptr_t i = 0; i < kHeaderSize; i++) {
|
||||
object = data_.At(i);
|
||||
new_data.SetAt(i, object);
|
||||
}
|
||||
// Then mark all the entries in new_data as unoccupied.
|
||||
smi_handle_ = Sentinel();
|
||||
InstantiationsCacheTable to_table(new_data);
|
||||
for (const auto& tuple : to_table) {
|
||||
tuple.Set<kSentinelIndex>(smi_handle_);
|
||||
}
|
||||
// Finally, copy over the entries.
|
||||
auto& function_tav = TypeArguments::Handle(zone_);
|
||||
auto& result_tav = TypeArguments::Handle(zone_);
|
||||
const InstantiationsCacheTable from_table(data_);
|
||||
for (const auto& from_tuple : from_table) {
|
||||
// Skip unoccupied entries.
|
||||
if (from_tuple.Get<kSentinelIndex>() == Sentinel()) continue;
|
||||
object = from_tuple.Get<kInstantiatorTypeArgsIndex>();
|
||||
const auto& instantiator_tav = TypeArguments::Cast(object);
|
||||
function_tav = from_tuple.Get<kFunctionTypeArgsIndex>();
|
||||
result_tav = from_tuple.Get<kInstantiatedTypeArgsIndex>();
|
||||
// Since new_data has a different total capacity, we can't use the old
|
||||
// entry indexes, but must recalculate them.
|
||||
auto loc = FindKeyOrUnused(new_data, instantiator_tav, function_tav);
|
||||
ASSERT(!loc.present);
|
||||
const auto& to_tuple = to_table.At(loc.entry);
|
||||
to_tuple.Set<kInstantiatorTypeArgsIndex>(instantiator_tav);
|
||||
to_tuple.Set<kFunctionTypeArgsIndex>(function_tav);
|
||||
to_tuple.Set<kInstantiatedTypeArgsIndex>(result_tav);
|
||||
}
|
||||
data_ = new_data.ptr();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TypeArguments::HasInstantiations() const {
|
||||
return instantiations() != Cache::EmptyStorage().ptr();
|
||||
}
|
||||
|
||||
ArrayPtr TypeArguments::instantiations() const {
|
||||
@@ -7078,28 +7364,11 @@ TypeArgumentsPtr TypeArguments::InstantiateAndCanonicalizeFrom(
|
||||
ASSERT(function_type_arguments.IsNull() ||
|
||||
function_type_arguments.IsCanonical());
|
||||
// Lookup instantiators and if found, return instantiated result.
|
||||
Array& prior_instantiations = Array::Handle(zone, instantiations());
|
||||
ASSERT(!prior_instantiations.IsNull() && prior_instantiations.IsArray());
|
||||
// The instantiations cache is initialized with Object::zero_array() and is
|
||||
// therefore guaranteed to contain kNoInstantiator. No length check needed.
|
||||
ASSERT(prior_instantiations.Length() > 0); // Always at least a sentinel.
|
||||
intptr_t index = 0;
|
||||
while (true) {
|
||||
if ((prior_instantiations.At(
|
||||
index +
|
||||
TypeArguments::Instantiation::kInstantiatorTypeArgsIndex) ==
|
||||
instantiator_type_arguments.ptr()) &&
|
||||
(prior_instantiations.At(
|
||||
index + TypeArguments::Instantiation::kFunctionTypeArgsIndex) ==
|
||||
function_type_arguments.ptr())) {
|
||||
return TypeArguments::RawCast(prior_instantiations.At(
|
||||
index + TypeArguments::Instantiation::kInstantiatedTypeArgsIndex));
|
||||
}
|
||||
if (prior_instantiations.At(index) ==
|
||||
Smi::New(TypeArguments::kNoInstantiator)) {
|
||||
break;
|
||||
}
|
||||
index += TypeArguments::Instantiation::kSizeInWords;
|
||||
Cache cache(zone, *this);
|
||||
auto const loc = cache.FindKeyOrUnused(instantiator_type_arguments,
|
||||
function_type_arguments);
|
||||
if (loc.present) {
|
||||
return cache.Retrieve(loc.entry);
|
||||
}
|
||||
// Cache lookup failed. Instantiate the type arguments.
|
||||
TypeArguments& result = TypeArguments::Handle(zone);
|
||||
@@ -7109,44 +7378,9 @@ TypeArgumentsPtr TypeArguments::InstantiateAndCanonicalizeFrom(
|
||||
result = result.Canonicalize(thread, nullptr);
|
||||
// InstantiateAndCanonicalizeFrom is not reentrant. It cannot have been called
|
||||
// indirectly, so the prior_instantiations array cannot have grown.
|
||||
ASSERT(prior_instantiations.ptr() == instantiations());
|
||||
// Add instantiator and function type args and result to instantiations array.
|
||||
intptr_t length = prior_instantiations.Length();
|
||||
if ((index + TypeArguments::Instantiation::kSizeInWords) >= length) {
|
||||
// TODO(regis): Should we limit the number of cached instantiations?
|
||||
// Grow the instantiations array by about 50%, but at least by 1.
|
||||
// The initial array is Object::zero_array() of length 1.
|
||||
intptr_t entries =
|
||||
(length - 1) / TypeArguments::Instantiation::kSizeInWords;
|
||||
intptr_t new_entries = entries + (entries >> 1) + 1;
|
||||
length = new_entries * TypeArguments::Instantiation::kSizeInWords + 1;
|
||||
prior_instantiations =
|
||||
Array::Grow(prior_instantiations, length, Heap::kOld);
|
||||
set_instantiations(prior_instantiations);
|
||||
ASSERT((index + TypeArguments::Instantiation::kSizeInWords) < length);
|
||||
}
|
||||
|
||||
// Set sentinel marker at next position.
|
||||
prior_instantiations.SetAt(
|
||||
index + TypeArguments::Instantiation::kSizeInWords +
|
||||
TypeArguments::Instantiation::kInstantiatorTypeArgsIndex,
|
||||
Smi::Handle(zone, Smi::New(TypeArguments::kNoInstantiator)));
|
||||
|
||||
prior_instantiations.SetAt(
|
||||
index + TypeArguments::Instantiation::kFunctionTypeArgsIndex,
|
||||
function_type_arguments);
|
||||
prior_instantiations.SetAt(
|
||||
index + TypeArguments::Instantiation::kInstantiatedTypeArgsIndex, result);
|
||||
|
||||
// We let any concurrently running mutator thread now see the new entry by
|
||||
// using a store-release barrier.
|
||||
ASSERT(
|
||||
prior_instantiations.At(
|
||||
index + TypeArguments::Instantiation::kInstantiatorTypeArgsIndex) ==
|
||||
Smi::New(TypeArguments::kNoInstantiator));
|
||||
prior_instantiations.SetAtRelease(
|
||||
index + TypeArguments::Instantiation::kInstantiatorTypeArgsIndex,
|
||||
instantiator_type_arguments);
|
||||
ASSERT(cache.data_.ptr() == instantiations());
|
||||
cache.AddEntry(loc.entry, instantiator_type_arguments,
|
||||
function_type_arguments, result);
|
||||
return result.ptr();
|
||||
}
|
||||
|
||||
@@ -7167,10 +7401,9 @@ TypeArgumentsPtr TypeArguments::New(intptr_t len, Heap::Space space) {
|
||||
result.SetHash(0);
|
||||
result.set_nullability(0);
|
||||
}
|
||||
// The zero array should have been initialized.
|
||||
ASSERT(Object::zero_array().ptr() != Array::null());
|
||||
COMPILE_ASSERT(TypeArguments::kNoInstantiator == 0);
|
||||
result.set_instantiations(Object::zero_array());
|
||||
// The array used as storage for an empty linear cache should be initialized.
|
||||
ASSERT(Cache::EmptyStorage().ptr() != Array::null());
|
||||
result.set_instantiations(Cache::EmptyStorage());
|
||||
return result.ptr();
|
||||
}
|
||||
|
||||
|
||||
+170
-21
@@ -448,7 +448,7 @@ class Object {
|
||||
V(CompressedStackMaps, null_compressed_stackmaps) \
|
||||
V(TypeArguments, empty_type_arguments) \
|
||||
V(Array, empty_array) \
|
||||
V(Array, zero_array) \
|
||||
V(Array, empty_instantiations_cache_array) \
|
||||
V(ContextScope, empty_context_scope) \
|
||||
V(ObjectPool, empty_object_pool) \
|
||||
V(CompressedStackMaps, empty_compressed_stackmaps) \
|
||||
@@ -8106,28 +8106,173 @@ class TypeArguments : public Instance {
|
||||
const TypeArguments& instantiator_type_arguments,
|
||||
const TypeArguments& function_type_arguments) const;
|
||||
|
||||
// Each cached instantiation consists of a 3-tuple in the instantiations_
|
||||
// array stored in each canonical uninstantiated type argument vector.
|
||||
enum Instantiation {
|
||||
kInstantiatorTypeArgsIndex = 0,
|
||||
kFunctionTypeArgsIndex,
|
||||
kInstantiatedTypeArgsIndex,
|
||||
kSizeInWords,
|
||||
};
|
||||
class Cache : public ValueObject {
|
||||
public:
|
||||
// The contents of the backing array storage is a header followed by
|
||||
// a number of entry tuples. Any entry that is unoccupied has
|
||||
// Sentinel() as its first component.
|
||||
//
|
||||
// If the cache is linear, the entries can be accessed in a linear fashion:
|
||||
// all occupied entries come first, followed by at least one unoccupied
|
||||
// entry to mark the end of the cache. Guaranteeing at least one unoccupied
|
||||
// entry avoids the need for a length check when iterating over the contents
|
||||
// of the linear cache in stubs.
|
||||
//
|
||||
// If the cache is hash-based, the array is instead treated as a hash table
|
||||
// probed by using a hash value derived from the instantiator and function
|
||||
// type arguments.
|
||||
|
||||
// The array is terminated by the value kNoInstantiator occurring in place of
|
||||
// the instantiator type args of the 4-tuple that would otherwise follow.
|
||||
// Therefore, kNoInstantiator must be distinct from any type arguments vector,
|
||||
// even a null one. Since arrays are initialized with 0, the instantiations_
|
||||
// array is properly terminated upon initialization.
|
||||
static const intptr_t kNoInstantiator = 0;
|
||||
enum Header {
|
||||
// The number of occupied entries in the cache.
|
||||
kOccupiedEntriesIndex = 0,
|
||||
kHeaderSize,
|
||||
};
|
||||
|
||||
// The tuple of values stored in a given entry.
|
||||
//
|
||||
// Note: accesses of the first component outside of the type arguments
|
||||
// canonicalization mutex must have acquire semantics.
|
||||
enum Entry {
|
||||
kSentinelIndex = 0, // Used when only checking for sentinel values.
|
||||
kInstantiatorTypeArgsIndex = kSentinelIndex,
|
||||
kFunctionTypeArgsIndex,
|
||||
kInstantiatedTypeArgsIndex,
|
||||
kEntrySize,
|
||||
};
|
||||
|
||||
// Requires that the type arguments canonicalization mutex is held.
|
||||
Cache(Zone* zone, const TypeArguments& source);
|
||||
|
||||
// Requires that the type arguments canonicalization mutex is held.
|
||||
Cache(Zone* zone, const Array& array);
|
||||
|
||||
// Used to check that the state of the backing array is valid.
|
||||
//
|
||||
// Requires that the type arguments canonicalization mutex is held.
|
||||
DEBUG_ONLY(static bool IsValidStorageLocked(const Array& array);)
|
||||
|
||||
// Returns the number of entries stored in the cache.
|
||||
intptr_t NumOccupied() const { return NumOccupied(data_); }
|
||||
|
||||
struct KeyLocation {
|
||||
// The entry index if [present] is true, otherwise where the entry would
|
||||
// be located if added afterwards without any intermediate additions.
|
||||
intptr_t entry;
|
||||
bool present; // Whether an entry already exists in the cache.
|
||||
};
|
||||
|
||||
// If an entry contains the given instantiator and function type arguments,
|
||||
// returns a KeyLocation with the index of the entry and true. Otherwise,
|
||||
// returns the index an entry with those keys would have if added and false.
|
||||
KeyLocation FindKeyOrUnused(const TypeArguments& instantiator_tav,
|
||||
const TypeArguments& function_tav) const {
|
||||
return FindKeyOrUnused(data_, instantiator_tav, function_tav);
|
||||
}
|
||||
|
||||
// Returns whether the entry at the given index in the cache is occupied.
|
||||
bool IsOccupied(intptr_t entry) const;
|
||||
|
||||
// Given an occupied entry index, returns the instantiated TypeArguments.
|
||||
TypeArgumentsPtr Retrieve(intptr_t entry) const;
|
||||
|
||||
// Adds a new instantiation mapping to the cache at index [entry]. Assumes
|
||||
// that the entry at index [entry] is unoccupied.
|
||||
//
|
||||
// May replace the underlying storage array, in which case the returned
|
||||
// index of the entry may differ from the requested one. If this Cache was
|
||||
// constructed using a TypeArguments object, its instantiations field is
|
||||
// also updated to point to the new storage.
|
||||
KeyLocation AddEntry(intptr_t entry,
|
||||
const TypeArguments& instantiator_tav,
|
||||
const TypeArguments& function_tav,
|
||||
const TypeArguments& instantiated_tav) const;
|
||||
|
||||
// The sentinel value used to mark unoccupied entries.
|
||||
static SmiPtr Sentinel();
|
||||
|
||||
static const Array& EmptyStorage() {
|
||||
return Object::empty_instantiations_cache_array();
|
||||
}
|
||||
|
||||
// Returns whether the cache is linear.
|
||||
bool IsLinear() const { return IsLinear(data_); }
|
||||
|
||||
// Returns whether the cache is hash-based.
|
||||
bool IsHash() const { return IsHash(data_); }
|
||||
|
||||
private:
|
||||
static constexpr double LoadFactor(intptr_t occupied, intptr_t capacity) {
|
||||
return occupied / static_cast<double>(capacity);
|
||||
}
|
||||
|
||||
// Returns the number of entries stored in the cache backed by the given
|
||||
// array.
|
||||
static intptr_t NumOccupied(const Array& array);
|
||||
|
||||
// Returns whether the cache backed by the given storage is linear.
|
||||
static bool IsLinear(const Array& array) { return !IsHash(array); }
|
||||
|
||||
// Returns whether the cache backed by the given storage is hash-based.
|
||||
static bool IsHash(const Array& array);
|
||||
|
||||
// Ensures that the backing store for the cache can hold at least [occupied]
|
||||
// occupied entries. If it cannot, replaces the backing store with one that
|
||||
// can, copying over entries from the old backing store.
|
||||
//
|
||||
// Returns whether the backing store changed.
|
||||
bool EnsureCapacity(intptr_t occupied) const;
|
||||
|
||||
// Retrieves the number of entries (occupied or unoccupied) in the cache.
|
||||
intptr_t NumEntries() const { return NumEntries(data_); }
|
||||
|
||||
// Retrieves the number of entries (occupied or unoccupied) in a cache
|
||||
// backed by the given array.
|
||||
static intptr_t NumEntries(const Array& array);
|
||||
|
||||
// If an entry in the given array contains the given instantiator and
|
||||
// function type arguments, returns a KeyLocation with the index of the
|
||||
// entry and true. Otherwise, returns a KeyLocation with the index that
|
||||
// would be used if the instantiation for the the given type arguments is
|
||||
// added and false.
|
||||
static KeyLocation FindKeyOrUnused(const Array& array,
|
||||
const TypeArguments& instantiator_tav,
|
||||
const TypeArguments& function_tav);
|
||||
|
||||
// The sentinel value in the Smi returned from Sentinel().
|
||||
static constexpr intptr_t kSentinelValue = 0;
|
||||
|
||||
public:
|
||||
// The maximum number of occupied entries for a linear cache of
|
||||
// instantiations before swapping to a hash table-based cache.
|
||||
static constexpr intptr_t kMaxLinearCacheEntries = 500;
|
||||
|
||||
// The maximum size of the array backing a linear cache. All hash based
|
||||
// caches are guaranteed to have sizes larger than this.
|
||||
static constexpr intptr_t kMaxLinearCacheSize =
|
||||
kHeaderSize + (kMaxLinearCacheEntries + 1) * kEntrySize;
|
||||
|
||||
private:
|
||||
// The initial number of entries used when converting from a linear to
|
||||
// a hash-based cache.
|
||||
static constexpr intptr_t kNumInitialHashCacheEntries =
|
||||
Utils::RoundUpToPowerOfTwo(2 * kMaxLinearCacheEntries);
|
||||
static_assert(Utils::IsPowerOfTwo(kNumInitialHashCacheEntries),
|
||||
"number of hash-based cache entries must be a power of two");
|
||||
|
||||
// The max load factor allowed in hash-based caches.
|
||||
static constexpr double kMaxLoadFactor = 0.71;
|
||||
|
||||
Zone* const zone_;
|
||||
const TypeArguments* const cache_container_;
|
||||
Array& data_;
|
||||
Smi& smi_handle_;
|
||||
|
||||
friend class TypeArguments; // For asserts against data_.
|
||||
};
|
||||
|
||||
// Return true if this type argument vector has cached instantiations.
|
||||
bool HasInstantiations() const;
|
||||
|
||||
// Return the number of cached instantiations for this type argument vector.
|
||||
intptr_t NumInstantiations() const;
|
||||
|
||||
static intptr_t instantiations_offset() {
|
||||
return OFFSET_OF(UntaggedTypeArguments, instantiations_);
|
||||
}
|
||||
@@ -12924,10 +13069,10 @@ class ArrayOfTuplesView {
|
||||
TupleView entry_;
|
||||
};
|
||||
|
||||
explicit ArrayOfTuplesView(const Array& array) : array_(array), index_(-1) {
|
||||
explicit ArrayOfTuplesView(const Array& array) : array_(array) {
|
||||
ASSERT(!array.IsNull());
|
||||
ASSERT(array.Length() >= kStartOffset);
|
||||
ASSERT((array.Length() - kStartOffset) % EntrySize == kStartOffset);
|
||||
ASSERT(array.Length() % EntrySize == kStartOffset);
|
||||
}
|
||||
|
||||
intptr_t Length() const {
|
||||
@@ -12948,7 +13093,6 @@ class ArrayOfTuplesView {
|
||||
|
||||
private:
|
||||
const Array& array_;
|
||||
intptr_t index_;
|
||||
};
|
||||
|
||||
using InvocationDispatcherTable =
|
||||
@@ -12973,6 +13117,11 @@ using SubtypeTestCacheTable = ArrayOfTuplesView<SubtypeTestCache::Entries,
|
||||
using MegamorphicCacheEntries =
|
||||
ArrayOfTuplesView<MegamorphicCache::EntryType, std::tuple<Smi, Object>>;
|
||||
|
||||
using InstantiationsCacheTable =
|
||||
ArrayOfTuplesView<TypeArguments::Cache::Entry,
|
||||
std::tuple<Object, TypeArguments, TypeArguments>,
|
||||
TypeArguments::Cache::kHeaderSize>;
|
||||
|
||||
void DumpTypeTable(Isolate* isolate);
|
||||
void DumpTypeParameterTable(Isolate* isolate);
|
||||
void DumpTypeArgumentsTable(Isolate* isolate);
|
||||
|
||||
@@ -223,23 +223,19 @@ void TypeArguments::PrintJSONImpl(JSONStream* stream, bool ref) const {
|
||||
}
|
||||
if (!IsInstantiated()) {
|
||||
JSONArray jsarr(&jsobj, "_instantiations");
|
||||
Array& prior_instantiations = Array::Handle(instantiations());
|
||||
ASSERT(prior_instantiations.Length() > 0); // Always at least a sentinel.
|
||||
TypeArguments& type_args = TypeArguments::Handle();
|
||||
intptr_t i = 0;
|
||||
while (prior_instantiations.At(i) !=
|
||||
Smi::New(TypeArguments::kNoInstantiator)) {
|
||||
Array& prior_instantiations = Array::Handle(zone, instantiations());
|
||||
TypeArguments& type_args = TypeArguments::Handle(zone);
|
||||
InstantiationsCacheTable table(prior_instantiations);
|
||||
for (const auto& tuple : table) {
|
||||
// Skip unoccupied entries.
|
||||
if (tuple.Get<Cache::kSentinelIndex>() == Cache::Sentinel()) continue;
|
||||
JSONObject instantiation(&jsarr);
|
||||
type_args ^= prior_instantiations.At(
|
||||
i + TypeArguments::Instantiation::kInstantiatorTypeArgsIndex);
|
||||
type_args ^= tuple.Get<Cache::kInstantiatorTypeArgsIndex>();
|
||||
instantiation.AddProperty("instantiatorTypeArguments", type_args, true);
|
||||
type_args ^= prior_instantiations.At(
|
||||
i + TypeArguments::Instantiation::kFunctionTypeArgsIndex);
|
||||
type_args = tuple.Get<Cache::kFunctionTypeArgsIndex>();
|
||||
instantiation.AddProperty("functionTypeArguments", type_args, true);
|
||||
type_args ^= prior_instantiations.At(
|
||||
i + TypeArguments::Instantiation::kInstantiatedTypeArgsIndex);
|
||||
type_args = tuple.Get<Cache::kInstantiatedTypeArgsIndex>();
|
||||
instantiation.AddProperty("instantiated", type_args, true);
|
||||
i += TypeArguments::Instantiation::kSizeInWords;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+153
-4
@@ -1916,15 +1916,28 @@ ISOLATE_UNIT_TEST_CASE(Array) {
|
||||
|
||||
EXPECT_EQ(0, Object::empty_array().Length());
|
||||
|
||||
EXPECT_EQ(1, Object::zero_array().Length());
|
||||
element = Object::zero_array().At(0);
|
||||
EXPECT(Smi::Cast(element).IsZero());
|
||||
|
||||
array.MakeImmutable();
|
||||
Object& obj = Object::Handle(array.ptr());
|
||||
EXPECT(obj.IsArray());
|
||||
}
|
||||
|
||||
ISOLATE_UNIT_TEST_CASE(EmptyInstantiationsCacheArray) {
|
||||
SafepointMutexLocker ml(
|
||||
thread->isolate_group()->type_arguments_canonicalization_mutex());
|
||||
const Array& empty_cache = Object::empty_instantiations_cache_array();
|
||||
DEBUG_ONLY(EXPECT(TypeArguments::Cache::IsValidStorageLocked(empty_cache));)
|
||||
const TypeArguments::Cache cache(thread->zone(), empty_cache);
|
||||
EXPECT(cache.IsLinear());
|
||||
EXPECT(!cache.IsHash());
|
||||
EXPECT_EQ(0, cache.NumOccupied());
|
||||
const InstantiationsCacheTable table(empty_cache);
|
||||
EXPECT_EQ(1, table.Length());
|
||||
for (const auto& tuple : table) {
|
||||
EXPECT(tuple.Get<TypeArguments::Cache::kSentinelIndex>() ==
|
||||
TypeArguments::Cache::Sentinel());
|
||||
}
|
||||
}
|
||||
|
||||
static void TestIllegalArrayLength(intptr_t length) {
|
||||
char buffer[1024];
|
||||
Utils::SNPrint(buffer, sizeof(buffer),
|
||||
@@ -7990,6 +8003,142 @@ FutureOr<T?> bar<T>() { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
static void TypeArgumentsHashCacheTest(Thread* thread, intptr_t num_classes) {
|
||||
TextBuffer buffer(MB);
|
||||
buffer.AddString("class D<T> {}\n");
|
||||
for (intptr_t i = 0; i < num_classes; i++) {
|
||||
buffer.Printf("class C%" Pd " { String toString() => 'C%" Pd "'; }\n", i,
|
||||
i);
|
||||
}
|
||||
buffer.AddString("main() {\n");
|
||||
for (intptr_t i = 0; i < num_classes; i++) {
|
||||
buffer.Printf(" new C%" Pd "().toString();\n", i);
|
||||
}
|
||||
buffer.AddString("}\n");
|
||||
|
||||
Dart_Handle api_lib = TestCase::LoadTestScript(buffer.buffer(), NULL);
|
||||
EXPECT_VALID(api_lib);
|
||||
Dart_Handle result = Dart_Invoke(api_lib, NewString("main"), 0, NULL);
|
||||
EXPECT_VALID(result);
|
||||
|
||||
// D + C0...CN, where N = kNumClasses - 1
|
||||
EXPECT(IsolateGroup::Current()->class_table()->NumCids() > num_classes);
|
||||
|
||||
TransitionNativeToVM transition(thread);
|
||||
Zone* const zone = thread->zone();
|
||||
|
||||
const auto& root_lib =
|
||||
Library::CheckedHandle(zone, Api::UnwrapHandle(api_lib));
|
||||
EXPECT(!root_lib.IsNull());
|
||||
|
||||
const auto& class_d = Class::Handle(zone, GetClass(root_lib, "D"));
|
||||
ASSERT(!class_d.IsNull());
|
||||
const auto& decl_type_d = Type::Handle(zone, class_d.DeclarationType());
|
||||
const auto& decl_type_d_type_args =
|
||||
TypeArguments::Handle(zone, decl_type_d.arguments());
|
||||
|
||||
EXPECT(!decl_type_d_type_args.HasInstantiations());
|
||||
|
||||
auto& class_c = Class::Handle(zone);
|
||||
auto& decl_type_c = Type::Handle(zone);
|
||||
auto& instantiator_type_args = TypeArguments::Handle(zone);
|
||||
const auto& function_type_args = Object::null_type_arguments();
|
||||
auto& result_type_args = TypeArguments::Handle(zone);
|
||||
auto& result_type = AbstractType::Handle(zone);
|
||||
// Cache the first computed set of instantiator type arguments to check that
|
||||
// no entries from the cache have been lost when the cache grows.
|
||||
auto& first_instantiator_type_args = TypeArguments::Handle(zone);
|
||||
for (intptr_t i = 0; i < num_classes; ++i) {
|
||||
auto const name = OS::SCreate(zone, "C%" Pd "", i);
|
||||
class_c = GetClass(root_lib, name);
|
||||
ASSERT(!class_c.IsNull());
|
||||
decl_type_c = class_c.DeclarationType();
|
||||
instantiator_type_args = TypeArguments::New(1);
|
||||
instantiator_type_args.SetTypeAt(0, decl_type_c);
|
||||
instantiator_type_args = instantiator_type_args.Canonicalize(thread);
|
||||
|
||||
// Check that the key does not currently exist in the cache.
|
||||
{
|
||||
SafepointMutexLocker ml(
|
||||
thread->isolate_group()->type_arguments_canonicalization_mutex());
|
||||
TypeArguments::Cache cache(zone, decl_type_d_type_args);
|
||||
EXPECT_EQ(i, cache.NumOccupied());
|
||||
auto loc =
|
||||
cache.FindKeyOrUnused(instantiator_type_args, function_type_args);
|
||||
EXPECT(!loc.present);
|
||||
}
|
||||
|
||||
decl_type_d_type_args.InstantiateAndCanonicalizeFrom(instantiator_type_args,
|
||||
function_type_args);
|
||||
|
||||
// Check that the key now does exist in the cache.
|
||||
TypeArguments::Cache::KeyLocation loc;
|
||||
{
|
||||
SafepointMutexLocker ml(
|
||||
thread->isolate_group()->type_arguments_canonicalization_mutex());
|
||||
TypeArguments::Cache cache(zone, decl_type_d_type_args);
|
||||
EXPECT_EQ(i + 1, cache.NumOccupied());
|
||||
// Double-check that we got the expected type of cache.
|
||||
EXPECT(i < TypeArguments::Cache::kMaxLinearCacheEntries ? cache.IsLinear()
|
||||
: cache.IsHash());
|
||||
loc = cache.FindKeyOrUnused(instantiator_type_args, function_type_args);
|
||||
EXPECT(loc.present);
|
||||
}
|
||||
|
||||
result_type_args = decl_type_d_type_args.InstantiateAndCanonicalizeFrom(
|
||||
instantiator_type_args, function_type_args);
|
||||
result_type = result_type_args.TypeAt(0);
|
||||
EXPECT_TYPES_SYNTACTICALLY_EQUIVALENT(decl_type_c, result_type);
|
||||
|
||||
// Check that no new entries were added to the cache.
|
||||
{
|
||||
SafepointMutexLocker ml(
|
||||
thread->isolate_group()->type_arguments_canonicalization_mutex());
|
||||
TypeArguments::Cache cache(zone, decl_type_d_type_args);
|
||||
EXPECT_EQ(i + 1, cache.NumOccupied());
|
||||
auto const loc2 =
|
||||
cache.FindKeyOrUnused(instantiator_type_args, function_type_args);
|
||||
EXPECT(loc2.present);
|
||||
EXPECT_EQ(loc.entry, loc2.entry);
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
first_instantiator_type_args = instantiator_type_args.ptr();
|
||||
} else {
|
||||
// Check that the first instantiator TAV still exists in the cache.
|
||||
SafepointMutexLocker ml(
|
||||
thread->isolate_group()->type_arguments_canonicalization_mutex());
|
||||
TypeArguments::Cache cache(zone, decl_type_d_type_args);
|
||||
EXPECT_EQ(i + 1, cache.NumOccupied());
|
||||
// Double-check that we got the expected type of cache.
|
||||
EXPECT(i < TypeArguments::Cache::kMaxLinearCacheEntries ? cache.IsLinear()
|
||||
: cache.IsHash());
|
||||
auto const loc =
|
||||
cache.FindKeyOrUnused(instantiator_type_args, function_type_args);
|
||||
EXPECT(loc.present);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A smaller version of the following test case, just to ensure some coverage
|
||||
// on slower builds.
|
||||
TEST_CASE(TypeArguments_Cache_SomeInstantiations) {
|
||||
TypeArgumentsHashCacheTest(thread,
|
||||
2 * TypeArguments::Cache::kMaxLinearCacheEntries);
|
||||
}
|
||||
|
||||
// Too slow in debug mode. Also avoid the sanitizers for similar reasons.
|
||||
#if !defined(DEBUG) && !defined(USING_MEMORY_SANITIZER) && \
|
||||
!defined(USING_THREAD_SANITIZER) && !defined(USING_LEAK_SANITIZER) && \
|
||||
!defined(USING_UNDEFINED_BEHAVIOR_SANITIZER)
|
||||
TEST_CASE(TypeArguments_Cache_ManyInstantiations) {
|
||||
const intptr_t kNumClasses = 100000;
|
||||
static_assert(kNumClasses > TypeArguments::Cache::kMaxLinearCacheEntries,
|
||||
"too few classes to trigger change to a hash-based cache");
|
||||
TypeArgumentsHashCacheTest(thread, kNumClasses);
|
||||
}
|
||||
#endif
|
||||
|
||||
#undef EXPECT_TYPES_SYNTACTICALLY_EQUIVALENT
|
||||
|
||||
} // namespace dart
|
||||
|
||||
Reference in New Issue
Block a user