quickjs speed increases #37
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Feature / opcode group Why it costs you today Easy way to drop it What breaks
1 Non-strict/“sloppy” mode
push_this autobox,
arguments mapped object, implicit globals, “with”. Every call into JS_CallInternal has a branch to decide whether to box this. Extra opcodes/services for arguments & with have to stay linked even if you never use them. Turn the parser default to strict and delete:
• OP_push_this non-strict path
• OP_SPECIAL_OBJECT_ARGUMENTS/ MAPPED_ARGUMENTS
• whole OP_with_* family and js_has_unscopable. Scripts that rely on legacy JS sloppiness, with, implicit globals, or the quirky arguments aliasing.
2 eval (direct & indirect) OP_eval, OP_apply_eval + runtime re-compiler Forces the engine to keep the compiler linked into every build and to keep scope objects live (the scope_idx dance) so that an eval might touch them. Make the parser reject eval(, patch the emitter so it never produces the two opcodes, and rip them (and js_same_value(ctx, …, ctx->eval_obj)) out of the loop. Any run-time string compilation (including Function constructor).
3 Generators / async / await (OP_yield*, OP_await, state machine in the call prologue) You pay for the if (b->func_kind != JS_FUNC_NORMAL) tail path on every call; the stack-frame resurrection machinery drags in Promise helpers. Skip byte-code emission for generator / async functions; delete the FUNC_RET_* section and the if (b->func_kind …) tail. function*, async function, for-await-of, yield, await.
4 BigInt / BigFloat / BigDecimal OP_push_bigint_i32 and CONFIG_BIGNUM branches in every numeric slow-path Adds an alternate numeric tag and type checks in all arithmetic, comparison and bit-op slow paths. Build with #undef CONFIG_BIGNUM, drop OP_push_bigint_i32, JS_NewShortBigInt, and any JS_TAG_BIG_INT/DEC/FLOAT cases. 123n, ** 2n, 1n << 3n, etc.
5 Private fields & class brand checks OP_private*, OP_check_brand, JS_AddBrand Class objects carry a hidden WeakSet; every new and every property access on a private‐field class hits an extra check. Yank OP_private*, OP_check_brand, JS_AddBrand, and stub out JS_NewSymbolFromAtom(..., JS_ATOM_TYPE_PRIVATE). #x private fields, class { #x }.
6 The “arguments” object altogether Even in strict mode the emitter will still generate OP_special_object_arguments if code mentions it; creating that array is costly. Patch the parser so arguments is a reserved word that throws, delete the two SPECIAL_OBJECT_ARGUMENTS cases and the helper js_build_arguments. Any code that touches arguments.
7 for…in, generic for…of, iterator protocol OP_for_in*, OP_iterator, Symbol.iterator helpers To support spec-compliant iteration QJS has to allocate helper objects and check for throw/return. Emit a simple counted loop for arrays and strings, remove the iterator opcodes & helpers, and drop JS_IteratorClose/friends. for (k in obj), user-defined iterators, destructuring of iterables, spread (...obj), yield.
8 Delete semantics OP_delete, OP_delete_var delete must call into JS_DeleteProperty() which walks prototypes and can de-opt hidden-classes. Make delete a syntax error; remove the opcodes and their slow path. Any use of delete.
9 Multiple numeric tags (int vs float64) Every numeric op starts with two JS_VALUE_IS_BOTH_INT() checks. If you are really bold: store all numbers as IEEE-754 double (Lua-style). Remove JS_TAG_INT and the paired fast/slow paths. 32-bit-int only tricks (bit-ops still work but low-level tag assumptions change); you’ll touch a lot of code, so keep for later.
2 | Feature “hit list” for faster property reads
Feature / flag / path What you drop from hot path How to amputate What breaks
A JS_PROP_GETSET (ES6 getters / setters) • Branch + call to JS
• Ref-count of function object Change parser/emitter to reject get foo() {} / set bar(v){} and delete the JS_PROP_GETSET case. Any getter/setter syntax, Proxy traps that produce accessor descriptors.
B JS_PROP_VARREF (closure-captured vars) • Branch + indirection through var_ref pointer Emit closure variables as copies, not live references. Delete JS_PROP_VARREF path and get_var_ref* opcodes. function f(){ let x=1; return ()=>x++ } live updates will break (captures become by-value).
C JS_PROP_AUTOINIT (Class fields / private auto-init) • Branch + call into JS_AutoInitProperty and retry Remove class-field initialisation opcodes; strip JS_PROP_AUTOINIT from shapes. class C { x = 1 } and private fields.
D Exotic-class hooks (class_array[].exotic->get_property) • Hook lookup + virtual call per miss ctx->rt->class_array[id].exotic = NULL for all ids, delete check block. Keep only plain objects & arrays. Module Namespace, Proxy, Date, RegExp, arguments, etc. lose spec-correct behaviour – they behave like plain objects.
E Fast-array index path (p->fast_array) • Numeric-index check, typed-array bounds call If your game script uses only sparse or plain arrays, compile QuickJS with #undef CONFIG_FAST_ARRAY. Dense JS arrays lose O(1) numeric access; they degrade to shape look-ups. (Often still fine in small game scripts.)
F String primitive property emulation ("foo".length, "foo"[3]) • Early switch-case + branch Force scripts to call .lengthOf(s) helpers or new String(). Delete JS_TAG_STRING / STRING_ROPE cases. "abc".length and indexing into literals stop working.
G TypedArray numeric-index early exit • Class-id range test + JS_AtomIsNumericIndex Build without TypedArray classes (they’re big anyway). All new Uint8Array() etc.
H Prototype chain beyond one level • while(p->shape->proto) loop (Radical) Flatten prototype after first property miss (Object.setPrototypeOf becomes unsupported) or disallow setting prototypes. Inheritance via prototype chain.
2 | Features you can delete (and what they cost)
Feature you remove How many cycles disappear One-line summary
A Back-traceable stack frame (JSStackFrame push/pop) ~7 If you never print JS stack traces over C functions, use #define JS_NO_CFUNC_FRAMES and turn the two assignments into no-ops.
B Stack-overflow probe ~5 If your engine runs in a fixed-depth script loop, replace with an assert() or kill it wholesale.
C Realm switching ~2 If you only run one realm, hard-wire ctx = caller_ctx and delete the line.
D fn_start/end hooks ~3–4 Compile out the branches; you can keep a global if (trace) inside your own C body instead.
E argc < length padding 0–8 Require native stubs to tolerate missing args (they all do in practice) and delete the alloca & loop.
F Multiple prototypes (constructor*, getter*, iterator_next*, _magic, f_f) 2–12 Keep only JS_CFUNC_generic; delete the switch and call straight through: ret_val = p->u.cfunc.c_function.generic(ctx,this_obj,argc,argv);
G Boxing helpers (JS_ToFloat64, JS_NewFloat64) 10–12 For math-heavy stubs, export your own double add(JSValueConst*,int) and call it inside JS, or use a custom opcode (see below).