Performance improvement opportunities from a QuickJS port (measured on Octane)
Maintainer thường phản hồi trong vòng 3 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 35/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- c, javascript
- Lĩnh vực
- backend, performance
Hướng nghiên cứu
Start by comparing the reported ideas with the current QuickJS sources, especially js_shape_prepare_update, close_var_refs, JS_MaybeGC, libregexp, and dtoa.c, which the issue names. Pick one optimization and first establish a benchmark and regression test for it; the issue provides Octane measurements but no QuickJS patch or specific test files. Done means the selected change is validated in QuickJS with correctness tests and reproducible performance measurements.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Hi! I've been working on silkjs (https://github.com/julia-script/silkjs), which started as a port of QuickJS in my language Silk (https://github.com/silk-lang/silk). I've been hill-climbing performance with AI, and I'm approaching jitless v8 performance.
I'm huge fan of QuickJS, it has been an amazing base to build on, so I wanted to share the changes that I think could carry over, in case any of them are useful. I'm happy to spend the tokens and open some PRs here if you prefer.
Here are the results I got
I checked each one against current master (VERSION 2026-06-04), so as far as I can tell none of these are there yet. The numbers are from silkjs, not QuickJS. Ir means callgrind instruction counts. Some gains were partly silkjs catching up on its own overhead, so expect smaller numbers in QuickJS for those.
Biggest wins
-
Per-site cache for get_field. A 4096-entry table keyed by bytecode address, holding a shape version and a slot index for the receiver's own shape and for prototypes up to 3 levels up. Own-shape hits alone gave Richards -18% wall time; depth-1 prototype hits added Richards -9% and DeltaBlue -11%. One catch: shapes edited in place (js_shape_prepare_update) need a version stamp, since the pointer alone is not a safe key. Caching put_field on existing properties gave nothing; caching property adds gave RayTrace -1.2% Ir.
-
Frameless calls for tiny functions. The compiler marks a function as a leaf when its body uses only side-effect-free opcodes, forward branches, and at most a put_field right before return. Calls to it run on scratch slots above the caller's frame, borrow values without refcount changes, and bail to the normal path on anything slow. A 64-miss budget turns it off for functions that keep bailing. DeltaBlue -25.6% Ir, Richards -7.1%. Most of the gain comes from borrowing; a version that kept refcounting was 2% slower.
-
Free closure cycles when a frame returns. In EarleyBoyer about 70% of what cycle collections scanned was dead pairs of a recursive local function and its own captured variable. When a frame with captured variables exits, we run a small trial deletion over up to 16 just-closed var refs and their closures, and free the dead ones right away. In QuickJS this would sit in close_var_refs. Earley went from 71 to 2 collections per 33 iterations, and from 8.5 to 4.8 ms per iteration.
-
Adaptive GC threshold. JS_MaybeGC always sets the next threshold to 1.5x the current size. If a collection frees less than 1/8 of the heap, we grow the factor up to 3x, since refcounting already handles acyclic garbage. Splay -12.5% Ir, and SplayLatency went from behind QuickJS to about 1.3x ahead. Peak memory did not change on the suite. This one is a few lines.
-
libregexp scan shortcuts. In order of gain on Octane RegExp:
- Skip ahead to the first position where the first consuming opcode (char, char_i, range, space, not_space, ^) can match, instead of trying every position. RegExp -30% Ir.
- A 32-byte Latin-1 bitmap next to the range table for classes.
- Literal and small-set search 16 bytes at a time (memchr or SSE2/NEON).
- Dedicated star/plus opcodes for
x*andx+over a single character, so they loop without the backtrack stack. - Flat alternation chains where each alternative jumps straight to the end (
/aaa|bbb|ccc/went from 1490 to 340 Ir in the matcher). - Skip save_reset when the quantifier max is 1 or less.
-
Shortest-digits dtoa. Schubfach for free-format radix-10 output (the TODO in dtoa.c mentions ryu): float to string -58% Ir. Our previous code, ported from the pinned QuickJS, was also not shortest for 54 powers of two, for example 2^-1016. I couldn't confirm that on master, so it may already be fixed. Parsing decimals with 15 or fewer digits as mantissa / 10^k (exact) gave string to float -54%.
Smaller ones
x == nullstill compiles to null + eq. Turning it into is_undefined_or_null gave Richards -2.4% Ir. Next to it,dup put_var x dropcould becomeput_var xfor globals (global writes 341 to 261 Ir). The optimizer already does this for put_loc, put_arg and put_var_ref.- Re-densify arrays.
while (--i >= 0) a[i] = 0on a fresh Array turns it into a slow array for good. Converting back once the properties are exactly length plus 0..len-1 gave Crypto -13% Ir. - Lazy arguments and call/apply forwarding. When
argumentsis only used asf.apply(x, arguments), skip building the object and apply on the frame's args. Enteringf.call(this, ...)andf.apply(this, arguments)inline instead of recursing gave RayTrace -17% Ir. Running the Prototype-stylethis.initialize.apply(this, arguments)constructor as the construct frame itself gave another -22%. - Objects born with their final shape. For constructors that only do
this.x = arg, cache the final shape per constructor and fill the object without a frame or per-property transitions. Doing the same for object literals at their bytecode site helped too. EarleyBoyer -17.8% Ir with the related caches, and Splay -22% together with a string-join change. - Fused loads.
get_loc; get_field,get_loc; get_lengthandget_loc a; get_loc i; get_array_elas single opcodes that read in place without retaining the receiver: Richards -4.7%, RayTrace -3.3%, EarleyBoyer -3.2% Ir. This has to be done at compile time. Peeking at the next opcode at run time made other code 10 to 13% slower. - RegExp builtins. When RegExp.prototype and the regexp still have their original shapes,
match,replace,split,execandtestcan skip the observableexec/flags/lastIndexlookups./x/gimatch went from 386 to 60 ms per 200K calls. - GC list walks. Prefetching two nodes ahead in each pass (Earley about 3% faster), and walking the third pass backwards since its order doesn't matter (simulated L2 misses 856K to 710K).
- Object spread. For a plain source with only data properties, adopt its shape and copy the property array in one go (2.06x to 0.74x of QuickJS's Ir on our clone microbenchmark).
- Small things: cached strings for integers 0 to 255, two digits per step in u32toa, an Int hash for Map/Set keys, in-place
x += yon uniquely held BigInts, aligning JS_CallInternal to 64 bytes (Crypto swung about 15% with its alignment), and polling interrupts only on backward jumps.
Things that did not help (in case they save you time)
- A put_field cache for existing properties, a global prototype-chain memo instead of per-site entries, and a 4x shape hash instead of 2x.
- Fusing Int compares with the following branch: fewer instructions, same or worse time.
- A young-generation cycle collection, and wider GC steps: no gain on Earley, worse on Splay.
- Prefetching a dying object's children, or freeing dead objects recursively on the spot: both slower.
- Hot-function link ordering and -Os: flat or worse overall.
- Tail duplication to unshare the dispatch jump: fewer mispredicts, but no time change.
Happy to share more detail, numbers or test cases for any of these. Thanks for QuickJS!
- Ngôn ngữ chính
- C
- Star
- 11.1k
- Fork
- 1.2k
- Merge trung bình
- 11 giờ 20 phút
- Pull request đã merge (30 ngày)
- 1
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của bellard/quickjs
-
Heap hardeningĐang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
Maintainer thường phản hồi trong vòng 3 ngày
-
`{ ..."ab" }` is `{}`; a throw in a `for await` body does not wait for the iterator's return()Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
Maintainer thường phản hồi trong vòng 3 ngày
-
Security audit of QuickJSĐang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 15/100
bellard/quickjs#610 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 3 ngày
-
What are the performance and memory optimization effects of using NaN-boxing versus not using it?Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
Maintainer thường phản hồi trong vòng 3 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
Maintainer thường phản hồi trong vòng 3 ngày
Tất cả issue của bellard/quickjs
Issue tương tự
-
Memory leak: "Add to Playlist" (`CMD_EVENT_ADD_TO_PLAYLIST`) never frees the playlist it loadsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Signed/unsigned mix in the EOCD backward scan rejects valid archives with a 65535-byte commentĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
-
area: recompiler area: runtime kind: bug preview-effort: small priority: low
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Prism::Translation::Parser raises NoMethodError for a method with a &nil parameter and a bodyCó thể đã có người làm @bbatsov đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày