← All issues

Async function promise optimization for no-await bodies

19dc01a

Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp

+ if (isEmptyBody()) {
+ ASSERT(!usingDeclarationCount());
+ ASSERT(!hasAwaitUsingDeclaration());
+ RefPtr<RegisterID> newResolvedPromise = generator.moveLinkTimeConstant(nullptr, LinkTimeConstant::newResolvedPromise);
+ CallArguments resolveArgs(generator, nullptr, 1);
+ generator.emitLoad(resolveArgs.thisRegister(), jsUndefined());
+ generator.emitLoad(resolveArgs.argumentRegister(0), jsUndefined());
+ RefPtr<RegisterID> result = generator.newTemporary();
+ generator.emitCall(result.get(), newResolvedPromise.get(), NoExpectedFunction, resolveArgs, divot, divot, divot, DebuggableCall::No);
+ generator.emitWillLeaveCallFrameDebugHook();
+ generator.emitReturn(result.get());
+ break;
+ }

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

+ case NewResolvedPromiseIntrinsic: {
+ if (argumentCountIncludingThis < 2)
+ return CallOptimizationResult::DidNothing;
+ insertChecks();
+ Node* argument = get(virtualRegisterForArgumentIncludingThis(1, registerOffset));
+ setResult(addToGraph(NewResolvedPromise, Edge(argument)));
+ return CallOptimizationResult::Inlined;
+ }
+
+ case NewRejectedPromiseIntrinsic: {
+ if (argumentCountIncludingThis < 2)
+ return CallOptimizationResult::DidNothing;
+ insertChecks();
+ Node* argument = get(virtualRegisterForArgumentIncludingThis(1, registerOffset));
+ setResult(addToGraph(NewRejectedPromise, Edge(argument)));
+ return CallOptimizationResult::Inlined;
+ }

JSC에서 모든 async 함수는 본문을 암묵적인 promise로 감싸는 state machine으로 변환됩니다. 기존 구현에서는 함수 진입 시점에 promise를 미리 할당하고, 본문 실행이 끝나면 fulfillPromiseWithFirstResolvingFunctionCallCheck를 호출해 정착시켰습니다. 이 경로에서는 thenable unwrapping, spec에서 요구하는 identity check, PerformPromiseThen 처리가 모두 수행됩니다. await가 없는 async 함수의 경우, promise는 반드시 첫 번째 반환 지점에서 동기적으로 정착합니다. 따라서 사전 할당과 전체 settle 경로는 순수한 overhead에 해당합니다.

이 commit은 컴파일 시점에 isEmptyBody()를 통해 await 없는 본문을 감지하고, 반환 시점에 newResolvedPromise 또는 newRejectedPromise를 호출하도록 코드를 생성합니다. 새로운 DFG/FTL IR 노드인 NewResolvedPromiseNewRejectedPromise가 이 경로를 지원하며, 반환값이 non-object임이 증명되면 JIT가 fulfilled promise 할당으로 직접 접어 최적화합니다.

Before (all async functions):
  Entry
   └─► allocate Promise (heap) ─────────────────────────────────┐
   └─► run body                                                  │
         └─► fulfillPromiseWithFirstResolvingFunctionCallCheck ──┘
              (thenable check, identity guard)
              └─► return pre-allocated Promise

After (no-await async functions):
  Entry
   └─► run body  (no promise allocated)
         ├── normal return ──► NewResolvedPromise(value)
         │                      (DFG: direct fulfilled-promise alloc if non-object)
         └── throw       ──► NewRejectedPromise(error)
              └─► return freshly-settled Promise

await가 없는 async 함수는 실제 코드베이스에서 흔히 등장합니다(adapter, delegation wrapper, 단순한 async API 등). 초기 promise 할당과 전체 settle 경로를 제거함으로써, 이러한 함수에 .then()을 체이닝하는 호출자 입장에서 GC 압력과 throughput overhead가 모두 줄어듭니다.

🔒

New JIT nodes and fast-path classification logic for async functions — several edge cases are worth security investigation.

더 확인하려면 구독해 주세요