LibJS: Skip iterator result object allocation in for..of and for..in

Introduce special instruction for `for..of` and `for..in` loop that
skips `{ value, done }` result object allocation if iterator is builtin
(array, map, set, string). This reduces GC pressure significantly and
avoids extracting the `value` and `done` properties.

This change makes this micro benchmark 48% faster on my computer:
```js
const arr = new Array(10_000_000);
let counter = 0;
for (let _ of arr) {
    counter++;
}
```
This commit is contained in:
Aliaksandr Kalenik 2025-04-30 16:31:26 +03:00 committed by Andreas Kling
parent ab52d86a69
commit 81b6a1100e
Notes: github-actions[bot] 2025-04-30 18:52:38 +00:00
4 changed files with 94 additions and 23 deletions

View file

@ -3183,11 +3183,23 @@ static Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> for_in_of_body_e
generator.begin_continuable_scope(Bytecode::Label { loop_update }, label_set);
// a. Let nextResult be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
auto next_value = generator.allocate_register();
auto done = generator.allocate_register();
if (iterator_kind == IteratorHint::Sync) {
generator.emit<Bytecode::Op::ForOfNext>(next_value, done, *head_result.iterator);
auto& loop_continue = generator.make_block();
generator.emit_jump_if(
done,
Bytecode::Label { loop_end },
Bytecode::Label { loop_continue });
generator.switch_to_basic_block(loop_continue);
} else {
auto next_result = generator.allocate_register();
generator.emit<Bytecode::Op::IteratorNext>(next_result, *head_result.iterator);
// b. If iteratorKind is async, set nextResult to ? Await(nextResult).
if (iterator_kind == IteratorHint::Async) {
auto received_completion = generator.allocate_register();
auto received_completion_type = generator.allocate_register();
auto received_completion_value = generator.allocate_register();
@ -3195,13 +3207,11 @@ static Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> for_in_of_body_e
generator.emit_mov(received_completion, generator.accumulator());
auto new_result = generate_await(generator, next_result, received_completion, received_completion_type, received_completion_value);
generator.emit_mov(next_result, new_result);
}
// c. If Type(nextResult) is not Object, throw a TypeError exception.
generator.emit<Bytecode::Op::ThrowIfNotObject>(next_result);
// d. Let done be ? IteratorComplete(nextResult).
auto done = generator.allocate_register();
generator.emit_iterator_complete(done, next_result);
// e. If done is true, return V.
@ -3213,8 +3223,8 @@ static Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> for_in_of_body_e
generator.switch_to_basic_block(loop_continue);
// f. Let nextValue be ? IteratorValue(nextResult).
auto next_value = generator.allocate_register();
generator.emit_iterator_value(next_value, next_result);
}
// g. If lhsKind is either assignment or varBinding, then
if (head_result.lhs_kind != LHSKind::LexicalBinding) {

View file

@ -51,6 +51,7 @@
O(EnterObjectEnvironment) \
O(EnterUnwindContext) \
O(Exp) \
O(ForOfNext) \
O(GetById) \
O(GetByIdWithThis) \
O(GetByValue) \

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2021-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -611,6 +612,7 @@ FLATTEN_ON_CLANG void Interpreter::run_bytecode(size_t entry_point)
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(Dump);
HANDLE_INSTRUCTION(EnterObjectEnvironment);
HANDLE_INSTRUCTION(Exp);
HANDLE_INSTRUCTION(ForOfNext);
HANDLE_INSTRUCTION(GetById);
HANDLE_INSTRUCTION(GetByIdWithThis);
HANDLE_INSTRUCTION(GetByValue);
@ -2980,6 +2982,27 @@ ThrowCompletionOr<void> IteratorNext::execute_impl(Bytecode::Interpreter& interp
return {};
}
ThrowCompletionOr<void> ForOfNext::execute_impl(Bytecode::Interpreter& interpreter) const
{
auto& vm = interpreter.vm();
auto& iterator_record = static_cast<IteratorRecord&>(interpreter.get(m_iterator_record).as_cell());
Value value;
bool done = false;
if (auto* builtin_iterator = iterator_record.iterator->as_builtin_iterator()) {
TRY(builtin_iterator->next(vm, done, value));
} else {
auto result = TRY(iterator_next(vm, iterator_record));
value = TRY(result->internal_get(vm.names.value, {}));
done = TRY(result->internal_get(vm.names.done, {})).to_boolean();
}
interpreter.set(dst_done(), Value(done));
interpreter.set(dst_value(), value);
return {};
}
ThrowCompletionOr<void> NewClass::execute_impl(Bytecode::Interpreter& interpreter) const
{
Value super_class;
@ -3754,6 +3777,14 @@ ByteString IteratorNext::to_byte_string_impl(Executable const& executable) const
format_operand("iterator_record"sv, m_iterator_record, executable));
}
ByteString ForOfNext::to_byte_string_impl(Executable const& executable) const
{
return ByteString::formatted("ForOfNext {}, {}, {}",
format_operand("dst_value"sv, m_dst_value, executable),
format_operand("dst_done"sv, m_dst_done, executable),
format_operand("iterator_record"sv, m_iterator_record, executable));
}
ByteString ResolveThisBinding::to_byte_string_impl(Bytecode::Executable const&) const
{
return "ResolveThisBinding"sv;

View file

@ -2721,6 +2721,35 @@ private:
Operand m_iterator_record;
};
class ForOfNext final : public Instruction {
public:
ForOfNext(Operand dst_value, Operand dst_done, Operand iterator_record)
: Instruction(Type::ForOfNext)
, m_dst_value(dst_value)
, m_dst_done(dst_done)
, m_iterator_record(iterator_record)
{
}
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_operands_impl(Function<void(Operand&)> visitor)
{
visitor(m_dst_value);
visitor(m_dst_done);
visitor(m_iterator_record);
}
Operand dst_value() const { return m_dst_value; }
Operand dst_done() const { return m_dst_done; }
Operand iterator_record() const { return m_iterator_record; }
private:
Operand m_dst_value;
Operand m_dst_done;
Operand m_iterator_record;
};
class ResolveThisBinding final : public Instruction {
public:
ResolveThisBinding()