mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-19 02:10:26 +00:00
LibJS: Add shape caching for object literal instantiation
When a function creates object literals with simple property names, we now cache the resulting shape after the first instantiation. On subsequent calls, we create the object with the cached shape directly and write property values at their known offsets. This avoids repeated shape transitions and property offset lookups for a common JavaScript pattern. The optimization uses two new bytecode instructions: - CacheObjectShape: Captures the final shape after object construction - InitObjectLiteralProperty: Writes properties using cached offsets Only "simple" object literals are optimized (string literal keys with simple value expressions). Complex cases like computed properties, getters/setters, and spread elements use the existing slow path. 3.4x speedup on a microbenchmark that repeatedly instantiates an object literal with 26 properties. Small progressions on various benchmarks.
This commit is contained in:
parent
b37ee5d356
commit
505fe0a977
Notes:
github-actions[bot]
2026-01-09 23:57:41 +00:00
Author: https://github.com/awesomekling
Commit: 505fe0a977
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7406
7 changed files with 117 additions and 2 deletions
|
|
@ -1151,12 +1151,28 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> ObjectExpression::gener
|
|||
|
||||
auto object = choose_dst(generator, preferred_dst);
|
||||
|
||||
generator.emit<Bytecode::Op::NewObject>(object);
|
||||
// Determine if this is a simple object literal (all KeyValue with StringLiteral keys)
|
||||
// Simple literals can benefit from shape caching with direct property offset writes.
|
||||
bool is_simple = !m_properties.is_empty();
|
||||
for (auto& property : m_properties) {
|
||||
if (property->type() != ObjectProperty::Type::KeyValue || !is<StringLiteral>(property->key())) {
|
||||
is_simple = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<u32> shape_cache_index;
|
||||
if (is_simple)
|
||||
shape_cache_index = generator.next_object_shape_cache();
|
||||
|
||||
generator.emit<Bytecode::Op::NewObject>(object, shape_cache_index.value_or(NumericLimits<u32>::max()));
|
||||
|
||||
if (m_properties.is_empty())
|
||||
return object;
|
||||
|
||||
generator.push_home_object(object);
|
||||
|
||||
u32 property_slot = 0;
|
||||
for (auto& property : m_properties) {
|
||||
Bytecode::PutKind property_kind;
|
||||
switch (property->type()) {
|
||||
|
|
@ -1195,7 +1211,13 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> ObjectExpression::gener
|
|||
}
|
||||
|
||||
auto property_key_table_index = generator.intern_property_key(string_literal.value());
|
||||
generator.emit_put_by_id(object, property_key_table_index, *value, property_kind, generator.next_property_lookup_cache());
|
||||
|
||||
// For simple object literals, use InitObjectLiteralProperty for direct offset writes
|
||||
if (is_simple) {
|
||||
generator.emit<Bytecode::Op::InitObjectLiteralProperty>(object, property_key_table_index, *value, *shape_cache_index, property_slot++);
|
||||
} else {
|
||||
generator.emit_put_by_id(object, property_key_table_index, *value, property_kind, generator.next_property_lookup_cache());
|
||||
}
|
||||
} else {
|
||||
auto property_name = TRY(property->key().generate_bytecode(generator)).value();
|
||||
auto value = TRY(generator.emit_named_evaluation_if_anonymous_function(property->value(), {}, {}, property->is_method()));
|
||||
|
|
@ -1205,6 +1227,10 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> ObjectExpression::gener
|
|||
}
|
||||
|
||||
generator.pop_home_object();
|
||||
|
||||
if (shape_cache_index.has_value())
|
||||
generator.emit<Bytecode::Op::CacheObjectShape>(object, *shape_cache_index);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -706,6 +706,7 @@ endop
|
|||
op NewObject < Instruction
|
||||
@nothrow
|
||||
m_dst: Operand
|
||||
m_cache_index: u32
|
||||
endop
|
||||
|
||||
op NewObjectWithNoPrototype < Instruction
|
||||
|
|
@ -1063,3 +1064,18 @@ op Yield < Instruction
|
|||
m_value: Operand
|
||||
endop
|
||||
|
||||
op CacheObjectShape < Instruction
|
||||
@nothrow
|
||||
m_object: Operand
|
||||
m_cache_index: u32
|
||||
endop
|
||||
|
||||
op InitObjectLiteralProperty < Instruction
|
||||
@nothrow
|
||||
m_object: Operand
|
||||
m_property: PropertyKeyTableIndex
|
||||
m_src: Operand
|
||||
m_shape_cache_index: u32
|
||||
m_property_slot: u32
|
||||
endop
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ Executable::Executable(
|
|||
size_t number_of_property_lookup_caches,
|
||||
size_t number_of_global_variable_caches,
|
||||
size_t number_of_template_object_caches,
|
||||
size_t number_of_object_shape_caches,
|
||||
size_t number_of_registers,
|
||||
Strict strict)
|
||||
: bytecode(move(bytecode))
|
||||
|
|
@ -42,6 +43,7 @@ Executable::Executable(
|
|||
property_lookup_caches.resize(number_of_property_lookup_caches);
|
||||
global_variable_caches.resize(number_of_global_variable_caches);
|
||||
template_object_caches.resize(number_of_template_object_caches);
|
||||
object_shape_caches.resize(number_of_object_shape_caches);
|
||||
}
|
||||
|
||||
Executable::~Executable() = default;
|
||||
|
|
|
|||
|
|
@ -76,6 +76,17 @@ struct TemplateObjectCache {
|
|||
GC::Ptr<Array> cached_template_object;
|
||||
};
|
||||
|
||||
// Cache for object literal shapes.
|
||||
// When an object literal like {a: 1, b: 2} is instantiated, we cache the final shape
|
||||
// so that subsequent instantiations can allocate the object with the correct shape directly,
|
||||
// avoiding repeated shape transitions.
|
||||
// We also cache the property offsets so that subsequent property writes can bypass
|
||||
// shape lookups and write directly to the correct storage slot.
|
||||
struct ObjectShapeCache {
|
||||
GC::Weak<Shape> shape;
|
||||
Vector<u32> property_offsets;
|
||||
};
|
||||
|
||||
struct SourceRecord {
|
||||
u32 source_start_offset {};
|
||||
u32 source_end_offset {};
|
||||
|
|
@ -97,6 +108,7 @@ public:
|
|||
size_t number_of_property_lookup_caches,
|
||||
size_t number_of_global_variable_caches,
|
||||
size_t number_of_template_object_caches,
|
||||
size_t number_of_object_shape_caches,
|
||||
size_t number_of_registers,
|
||||
Strict);
|
||||
|
||||
|
|
@ -107,6 +119,7 @@ public:
|
|||
Vector<PropertyLookupCache> property_lookup_caches;
|
||||
Vector<GlobalVariableCache> global_variable_caches;
|
||||
Vector<TemplateObjectCache> template_object_caches;
|
||||
Vector<ObjectShapeCache> object_shape_caches;
|
||||
NonnullOwnPtr<StringTable> string_table;
|
||||
NonnullOwnPtr<IdentifierTable> identifier_table;
|
||||
NonnullOwnPtr<PropertyKeyTable> property_key_table;
|
||||
|
|
|
|||
|
|
@ -456,6 +456,7 @@ CodeGenerationErrorOr<GC::Ref<Executable>> Generator::compile(VM& vm, ASTNode co
|
|||
generator.m_next_property_lookup_cache,
|
||||
generator.m_next_global_variable_cache,
|
||||
generator.m_next_template_object_cache,
|
||||
generator.m_next_object_shape_cache,
|
||||
generator.m_next_register,
|
||||
generator.m_strict);
|
||||
|
||||
|
|
|
|||
|
|
@ -349,6 +349,7 @@ public:
|
|||
[[nodiscard]] size_t next_global_variable_cache() { return m_next_global_variable_cache++; }
|
||||
[[nodiscard]] size_t next_property_lookup_cache() { return m_next_property_lookup_cache++; }
|
||||
[[nodiscard]] size_t next_template_object_cache() { return m_next_template_object_cache++; }
|
||||
[[nodiscard]] u32 next_object_shape_cache() { return m_next_object_shape_cache++; }
|
||||
|
||||
enum class DeduplicateConstant {
|
||||
Yes,
|
||||
|
|
@ -428,6 +429,7 @@ private:
|
|||
u32 m_next_property_lookup_cache { 0 };
|
||||
u32 m_next_global_variable_cache { 0 };
|
||||
u32 m_next_template_object_cache { 0 };
|
||||
u32 m_next_object_shape_cache { 0 };
|
||||
FunctionKind m_enclosing_function_kind { FunctionKind::Normal };
|
||||
Vector<LabelableScope> m_continuable_scopes;
|
||||
Vector<LabelableScope> m_breakable_scopes;
|
||||
|
|
|
|||
|
|
@ -551,6 +551,8 @@ void Interpreter::run_bytecode(size_t entry_point)
|
|||
HANDLE_INSTRUCTION(NewClass);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(NewFunction);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(NewObject);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(CacheObjectShape);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(InitObjectLiteralProperty);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(NewObjectWithNoPrototype);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(NewPrimitiveArray);
|
||||
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(NewRegExp);
|
||||
|
|
@ -1957,6 +1959,16 @@ void NewObject::execute_impl(Bytecode::Interpreter& interpreter) const
|
|||
{
|
||||
auto& vm = interpreter.vm();
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
if (m_cache_index != NumericLimits<u32>::max()) {
|
||||
auto& cache = interpreter.current_executable().object_shape_caches[m_cache_index];
|
||||
auto cached_shape = cache.shape.ptr();
|
||||
if (cached_shape) {
|
||||
interpreter.set(dst(), Object::create_with_premade_shape(*cached_shape));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
interpreter.set(dst(), Object::create(realm, realm.intrinsics().object_prototype()));
|
||||
}
|
||||
|
||||
|
|
@ -1967,6 +1979,49 @@ void NewObjectWithNoPrototype::execute_impl(Bytecode::Interpreter& interpreter)
|
|||
interpreter.set(dst(), Object::create(realm, nullptr));
|
||||
}
|
||||
|
||||
void CacheObjectShape::execute_impl(Bytecode::Interpreter& interpreter) const
|
||||
{
|
||||
auto& cache = interpreter.current_executable().object_shape_caches[m_cache_index];
|
||||
if (!cache.shape) {
|
||||
auto& object = interpreter.get(m_object).as_object();
|
||||
cache.shape = &object.shape();
|
||||
}
|
||||
}
|
||||
|
||||
COLD static void init_object_literal_property_slow(Object& object, PropertyKey const& property_key, Value value, ObjectShapeCache& cache, u32 property_slot)
|
||||
{
|
||||
object.define_direct_property(property_key, value, JS::Attribute::Enumerable | JS::Attribute::Writable | JS::Attribute::Configurable);
|
||||
|
||||
// Cache the property offset for future fast-path use
|
||||
// Note: lookup may fail if the shape is in dictionary mode or for other edge cases.
|
||||
// We only cache if we're not in dictionary mode and the lookup succeeds.
|
||||
if (!object.shape().is_dictionary()) {
|
||||
auto metadata = object.shape().lookup(property_key);
|
||||
if (metadata.has_value()) {
|
||||
if (property_slot >= cache.property_offsets.size())
|
||||
cache.property_offsets.resize(property_slot + 1);
|
||||
cache.property_offsets[property_slot] = metadata->offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InitObjectLiteralProperty::execute_impl(Bytecode::Interpreter& interpreter) const
|
||||
{
|
||||
auto& object = interpreter.get(m_object).as_object();
|
||||
auto value = interpreter.get(m_src);
|
||||
auto& cache = interpreter.current_executable().object_shape_caches[m_shape_cache_index];
|
||||
|
||||
// Fast path: if we have a cached shape and it matches, write directly to the cached offset
|
||||
auto cached_shape = cache.shape.ptr();
|
||||
if (cached_shape && &object.shape() == cached_shape && m_property_slot < cache.property_offsets.size()) {
|
||||
object.put_direct(cache.property_offsets[m_property_slot], value);
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& property_key = interpreter.current_executable().get_property_key(m_property);
|
||||
init_object_literal_property_slow(object, property_key, value, cache, m_property_slot);
|
||||
}
|
||||
|
||||
void NewRegExp::execute_impl(Bytecode::Interpreter& interpreter) const
|
||||
{
|
||||
interpreter.set(dst(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue