Bug 240984 - [JSC] Promise/async is about 6x slower than v8
Summary: [JSC] Promise/async is about 6x slower than v8
Status: NEW
Alias: None
Product: WebKit
Classification: Unclassified
Component: JavaScriptCore (show other bugs)
Version: WebKit Nightly Build
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Nobody
Keywords: InRadar
Depends on:
Reported: 2022-05-26 15:33 PDT by Jarred Sumner
Modified: 2022-06-28 09:01 PDT (History)
3 users (show)

See Also:

safari (46.76 KB, image/png)
2022-05-26 15:33 PDT, Jarred Sumner
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Jarred Sumner 2022-05-26 15:33:16 PDT
Created attachment 459797 [details]

Running https://github.com/v8/promise-performance-tests in jsc vs v8 reports the following on macOS arm64

with JSC:

❯ fd . --exec bun '{}'
Time(doxbee-async-es2017-native): 55.3 ms.
Time(doxbee-promises-es2015-native): 60.5 ms.
Time(doxbee-async-es2017-babel): 73.4 ms.
Time(parallel-promises-es2015-native): 255.8 ms.
Time(parallel-async-es2017-babel): 383.9 ms.
Time(parallel-async-es2017-native): 414.5 ms.
Time(fibonacci-async-es2017-babel): 785.8 ms.
Time(fibonacci-async-es2017-native): 1923.3 ms.

with V8:

❯ fd . --exec node '{}'
Time(doxbee-async-es2017-native): 20.9 ms.
Time(doxbee-promises-es2015-native): 26.6 ms.
Time(doxbee-async-es2017-babel): 42 ms.
Time(parallel-promises-es2015-native): 55.6 ms.
Time(parallel-async-es2017-native): 63.1 ms.
Time(parallel-async-es2017-babel): 82.7 ms.
Time(fibonacci-async-es2017-native): 110.9 ms.
Time(fibonacci-async-es2017-babel): 222.6 ms.

the bluebird ones were skipped because bun doesn't support node's async_hooks

In another microbench, we can run the following in jsc's shell with Mitata (https://github.com/evanwashere/mitata)

import {run, bench} from './mitata/src/cli.mjs';

bench("sync", () => {});
bench("async", async () => {});

await run();

That reports:

❯ node a.mjs
cpu: Apple M1 Max
runtime: node v18.2.0 (arm64-darwin)

benchmark      time (avg)             (min … max)       p75       p99      p995
------------------------------------------------- -----------------------------
sync       320.19 ps/iter  (304.1 ps … 258.77 ns)  316.7 ps  358.3 ps  370.8 ps
async       57.01 ns/iter  (52.72 ns … 122.63 ns)  59.06 ns 107.38 ns 115.63 ns

❯ jsc a.mjs -m
cpu: unknown
runtime: unknown (unknown)

benchmark      time (avg)             (min … max)       p75       p99      p995
------------------------------------------------- -----------------------------
sync       322.29 ps/iter     (300 ps … 15.65 ns)  316.7 ps  387.5 ps  416.6 ps
async      369.04 ns/iter (357.71 ns … 519.04 ns) 365.94 ns  476.6 ns 519.04 ns

Running this in Safari (attached screenshot) shows async to be about 565 ns/iter, something like 10x slower compared to V8/node

Running the https://github.com/v8/promise-performance-tests/blob/master/src/parallel-async-es2017-native.js with Instruments, shows this: 

We can see that a lot of time is spent in bookkeeping – preparing to call microtasks, creating JSC::JSMicrotask, destroying JSC::JSMicrotask, writing to handleset, etc
Comment 1 Jarred Sumner 2022-05-26 21:06:38 PDT
If we look at V8's implementation

We can see that they don't use a generic JSC::JSMicrotask-like type with a JS implementation. Instead, they have specific types for each job.


They also implement an optimization for resolving promises that skips allocating a temporary promise.
Comment 3 Radar WebKit Bug Importer 2022-06-02 15:34:12 PDT
Comment 4 Yusuke Suzuki 2022-06-10 10:35:47 PDT
(In reply to Jarred Sumner from comment #1)
> They also implement an optimization for resolving promises that skips
> allocating a temporary promise.
> https://github.com/v8/v8/blob/f32335fea75b7bde495e0800d7f7349253f81a7c/src/
> builtins/promise-jobs.tq#L15

Note that JSC is doing the same thing internally :)