LibJS: Let invokers (callers) of [[Call]] allocate ExecutionContext
Some checks are pending
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macos-15, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

Instead of letting every [[Call]] implementation allocate an
ExecutionContext, we now make that a responsibility of the caller.

The main point of this exercise is to allow the Call instruction
to write function arguments directly into the callee ExecutionContext
instead of copying them later.

This makes function calls significantly faster:
- 10-20% faster on micro-benchmarks (depending on argument count)
- 4% speedup on Kraken
- 2% speedup on Octane
- 5% speedup on JetStream
This commit is contained in:
Andreas Kling 2025-04-27 11:53:11 +02:00 committed by Andreas Kling
parent 93788f8057
commit a05be67e4a
Notes: github-actions[bot] 2025-04-27 23:24:56 +00:00
18 changed files with 139 additions and 84 deletions

View file

@ -110,7 +110,7 @@ NativeFunction::NativeFunction(FlyString name, Object& prototype)
// these good candidates for a bit of code duplication :^)
// 10.3.1 [[Call]] ( thisArgument, argumentsList ), https://tc39.es/ecma262/#sec-built-in-function-objects-call-thisargument-argumentslist
ThrowCompletionOr<Value> NativeFunction::internal_call(Value this_argument, ReadonlySpan<Value> arguments_list)
ThrowCompletionOr<Value> NativeFunction::internal_call(ExecutionContext& callee_context, Value this_argument)
{
auto& vm = this->vm();
@ -119,12 +119,10 @@ ThrowCompletionOr<Value> NativeFunction::internal_call(Value this_argument, Read
// 2. If callerContext is not already suspended, suspend callerContext.
// 3. Let calleeContext be a new execution context.
ExecutionContext* callee_context = nullptr;
ALLOCATE_EXECUTION_CONTEXT_ON_NATIVE_STACK(callee_context, 0, arguments_list.size());
// 4. Set the Function of calleeContext to F.
callee_context->function = this;
callee_context->function_name = m_name_string;
callee_context.function = this;
callee_context.function_name = m_name_string;
// 5. Let calleeRealm be F.[[Realm]].
auto callee_realm = m_realm;
@ -138,29 +136,27 @@ ThrowCompletionOr<Value> NativeFunction::internal_call(Value this_argument, Read
VERIFY(callee_realm);
// 6. Set the Realm of calleeContext to calleeRealm.
callee_context->realm = callee_realm;
callee_context.realm = callee_realm;
// 7. Set the ScriptOrModule of calleeContext to null.
// Note: This is already the default value.
// 8. Perform any necessary implementation-defined initialization of calleeContext.
callee_context->this_value = this_argument;
if (!arguments_list.is_empty())
callee_context->arguments.overwrite(0, arguments_list.data(), arguments_list.size() * sizeof(Value));
callee_context.this_value = this_argument;
callee_context->lexical_environment = caller_context.lexical_environment;
callee_context->variable_environment = caller_context.variable_environment;
callee_context.lexical_environment = caller_context.lexical_environment;
callee_context.variable_environment = caller_context.variable_environment;
// Note: Keeping the private environment is probably only needed because of async methods in classes
// calling async_block_start which goes through a NativeFunction here.
callee_context->private_environment = caller_context.private_environment;
callee_context.private_environment = caller_context.private_environment;
// NOTE: This is a LibJS specific hack for NativeFunction to inherit the strictness of its caller.
callee_context->is_strict_mode = vm.in_strict_mode();
callee_context.is_strict_mode = vm.in_strict_mode();
// </8.> --------------------------------------------------------------------------
// 9. Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
TRY(vm.push_execution_context(*callee_context, {}));
TRY(vm.push_execution_context(callee_context, {}));
// 10. Let result be the Completion Record that is the result of evaluating F in a manner that conforms to the specification of F. thisArgument is the this value, argumentsList provides the named parameters, and the NewTarget value is undefined.
auto result = call();
@ -184,6 +180,10 @@ ThrowCompletionOr<GC::Ref<Object>> NativeFunction::internal_construct(ReadonlySp
// 3. Let calleeContext be a new execution context.
ExecutionContext* callee_context = nullptr;
ALLOCATE_EXECUTION_CONTEXT_ON_NATIVE_STACK(callee_context, 0, arguments_list.size());
// 8. Perform any necessary implementation-defined initialization of calleeContext.
for (size_t i = 0; i < arguments_list.size(); ++i)
callee_context->arguments[i] = arguments_list[i];
callee_context->passed_argument_count = arguments_list.size();
// 4. Set the Function of calleeContext to F.
callee_context->function = this;
@ -206,10 +206,6 @@ ThrowCompletionOr<GC::Ref<Object>> NativeFunction::internal_construct(ReadonlySp
// 7. Set the ScriptOrModule of calleeContext to null.
// Note: This is already the default value.
// 8. Perform any necessary implementation-defined initialization of calleeContext.
if (!arguments_list.is_empty())
callee_context->arguments.overwrite(0, arguments_list.data(), arguments_list.size() * sizeof(Value));
callee_context->lexical_environment = caller_context.lexical_environment;
callee_context->variable_environment = caller_context.variable_environment;