Performance improvement opportunities from a QuickJS port (measured on Octane)
Los mantenedores suelen responder en 3 días
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- c, javascript
- Área
- backend, performance
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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!
- Lenguaje dominante
- C
- Estrellas
- 11.1k
- Forks
- 1.2k
- Merge medio
- 11 h 20 min
- PR fusionados (30 d)
- 1
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de bellard/quickjs
-
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
Los mantenedores suelen responder en 3 días
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
Los mantenedores suelen responder en 3 días
-
Dificultad 3/5 1-2 días Aptitud para principiantes 54/100
Los mantenedores suelen responder en 3 días
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 30/100
Los mantenedores suelen responder en 3 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
Los mantenedores suelen responder en 3 días
Todos los issues de bellard/quickjs
Issues similares
-
[Success Image Report]: GXDE OSAbierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 62/100
-
`PiecewiseChebyshevGate` produces incorrect target rotations outside the supplied breakpointsAbiertobug mod: circuit
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Los mantenedores suelen responder en 1 día
-
flac's CMake config is brokenAbiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
msys2/MINGW-packages#33356 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 1-3 horas Aptitud para principiantes 78/100
facebookincubator/muse-gadget-sdk#355 ·
Los mantenedores suelen responder en 4 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
kmod-project/kmod#492 ·
Los mantenedores suelen responder en 4 días