LibJS: Reduce inline cache memory usage

Store bytecode property lookup caches as tiered handles instead of
eagerly allocating four entries for every cache slot. Each slot starts
empty, grows to one monomorphic entry after the first cacheable lookup,
and promotes to the existing four-entry table when another cache key
appears.

Global variable caches keep one inline property entry because they
usually warm up as global object property accesses. This keeps common
global access allocation-free while still avoiding the inherited
polymorphic cache cost.

Generated asm offsets now point at the lazy property-cache storage and
the inline global entry, and asm fast paths bail out for empty property
cache slots.
This commit is contained in:
Andreas Kling 2026-05-18 13:57:57 +02:00 committed by Andreas Kling
parent 75b2bcd7e4
commit 0a49dd1c28
Notes: github-actions[bot] 2026-05-18 23:13:46 +00:00
7 changed files with 336 additions and 94 deletions

View file

@ -7,6 +7,7 @@
#include <AK/BinarySearch.h>
#include <AK/NumericLimits.h>
#include <AK/QuickSort.h>
#include <AK/StdLibExtras.h>
#include <LibGC/Heap.h>
#include <LibGC/HeapBlock.h>
#include <LibJS/Bytecode/BasicBlock.h>
@ -67,6 +68,152 @@ size_t InstructionStream::external_memory_size() const
});
}
static_assert(alignof(PropertyLookupCache::MonomorphicData) > PropertyLookupCache::polymorphic_data_tag);
static_assert(alignof(PropertyLookupCache::PolymorphicData) > PropertyLookupCache::polymorphic_data_tag);
static_assert(offsetof(PropertyLookupCache::MonomorphicData, entry) == 0);
static_assert(offsetof(PropertyLookupCache::PolymorphicData, entries) == 0);
PropertyLookupCache::PropertyLookupCache(PropertyLookupCache&& other)
: m_data(exchange(other.m_data, 0))
{
}
PropertyLookupCache& PropertyLookupCache::operator=(PropertyLookupCache&& other)
{
if (this != &other) {
clear();
m_data = exchange(other.m_data, 0);
}
return *this;
}
PropertyLookupCache::~PropertyLookupCache()
{
clear();
}
PropertyLookupCache::MonomorphicData* PropertyLookupCache::monomorphic_data()
{
if (!m_data || (m_data & polymorphic_data_tag))
return nullptr;
return reinterpret_cast<MonomorphicData*>(m_data);
}
PropertyLookupCache::MonomorphicData const* PropertyLookupCache::monomorphic_data() const
{
if (!m_data || (m_data & polymorphic_data_tag))
return nullptr;
return reinterpret_cast<MonomorphicData const*>(m_data);
}
PropertyLookupCache::PolymorphicData* PropertyLookupCache::polymorphic_data()
{
if (!(m_data & polymorphic_data_tag))
return nullptr;
return reinterpret_cast<PolymorphicData*>(m_data & ~polymorphic_data_tag);
}
PropertyLookupCache::PolymorphicData const* PropertyLookupCache::polymorphic_data() const
{
if (!(m_data & polymorphic_data_tag))
return nullptr;
return reinterpret_cast<PolymorphicData const*>(m_data & ~polymorphic_data_tag);
}
void PropertyLookupCache::set_monomorphic_data(MonomorphicData* data)
{
VERIFY(data);
VERIFY(!(reinterpret_cast<FlatPtr>(data) & polymorphic_data_tag));
m_data = reinterpret_cast<FlatPtr>(data);
}
void PropertyLookupCache::set_polymorphic_data(PolymorphicData* data)
{
VERIFY(data);
VERIFY(!(reinterpret_cast<FlatPtr>(data) & polymorphic_data_tag));
m_data = reinterpret_cast<FlatPtr>(data) | polymorphic_data_tag;
}
PropertyLookupCache::Entry* PropertyLookupCache::first_entry()
{
if (auto* data = monomorphic_data())
return &data->entry;
if (auto* data = polymorphic_data())
return &data->entries[0];
return nullptr;
}
PropertyLookupCache::Entry const* PropertyLookupCache::first_entry() const
{
if (auto* data = monomorphic_data())
return &data->entry;
if (auto* data = polymorphic_data())
return &data->entries[0];
return nullptr;
}
Span<PropertyLookupCache::Entry> PropertyLookupCache::entries()
{
if (auto* data = monomorphic_data())
return { &data->entry, 1 };
if (auto* data = polymorphic_data())
return data->entries.span();
return {};
}
ReadonlySpan<PropertyLookupCache::Entry> PropertyLookupCache::entries() const
{
if (auto* data = monomorphic_data())
return { &data->entry, 1 };
if (auto* data = polymorphic_data())
return data->entries.span();
return {};
}
size_t PropertyLookupCache::external_memory_size() const
{
if (monomorphic_data())
return sizeof(MonomorphicData);
if (polymorphic_data())
return sizeof(PolymorphicData);
return 0;
}
void PropertyLookupCache::clear()
{
if (auto* data = monomorphic_data()) {
delete data;
m_data = 0;
return;
}
if (auto* data = polymorphic_data()) {
delete data;
m_data = 0;
}
}
bool PropertyLookupCache::entries_have_same_cache_key(Entry const& a, Entry const& b)
{
if (a.type == Entry::Type::Empty || b.type == Entry::Type::Empty)
return false;
if (a.type != b.type)
return false;
switch (a.type) {
case Entry::Type::AddOwnProperty:
return a.from_shape == b.from_shape && a.shape == b.shape;
case Entry::Type::ChangeOwnProperty:
case Entry::Type::GetOwnProperty:
return a.shape == b.shape;
case Entry::Type::ChangePropertyInPrototypeChain:
case Entry::Type::GetPropertyInPrototypeChain:
return a.shape == b.shape && a.prototype == b.prototype;
case Entry::Type::Empty:
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
ObjectPropertyIteratorCacheData::ObjectPropertyIteratorCacheData(VM& vm, Vector<PropertyKey> properties, ObjectPropertyIteratorFastPath fast_path, u32 indexed_property_count, bool receiver_has_magical_length_property, GC::Ref<Shape> shape, GC::Ptr<PrototypeChainValidity> prototype_chain_validity)
: m_properties(move(properties))
, m_shape(shape)
@ -413,6 +560,8 @@ size_t Executable::external_memory_size() const
{
size_t size = bytecode.external_memory_size();
size = saturating_add_external_memory_size(size, vector_external_memory_size(property_lookup_caches));
for (auto const& cache : property_lookup_caches)
size = saturating_add_external_memory_size(size, cache.external_memory_size());
size = saturating_add_external_memory_size(size, vector_external_memory_size(global_variable_caches));
size = saturating_add_external_memory_size(size, vector_external_memory_size(template_object_caches));
size = saturating_add_external_memory_size(size, vector_external_memory_size(object_shape_caches));
@ -469,7 +618,7 @@ static void clear_cache_entry_if_dead(PropertyLookupCache::Entry& entry)
void StaticPropertyLookupCache::sweep_all()
{
for (auto* cache : static_property_lookup_caches()) {
for (auto& entry : cache->entries)
for (auto& entry : cache->entries())
clear_cache_entry_if_dead(entry);
}
}
@ -477,13 +626,11 @@ void StaticPropertyLookupCache::sweep_all()
void Executable::remove_dead_cells(Badge<GC::Heap>)
{
for (auto& cache : property_lookup_caches) {
for (auto& entry : cache.entries)
clear_cache_entry_if_dead(entry);
}
for (auto& cache : global_variable_caches) {
for (auto& entry : cache.entries)
for (auto& entry : cache.entries())
clear_cache_entry_if_dead(entry);
}
for (auto& cache : global_variable_caches)
clear_cache_entry_if_dead(cache.entry);
for (auto& cache : object_shape_caches) {
auto* shape = cache.shape.ptr();
if (shape && cell_is_dead(shape))