You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf: function-constructor objects (new F(), x instanceof F) are 78–566× slower than Node (F.prototype and builtin identity re-derived from side tables per operation) #10507
Found by the package performance audit (real npm packages compiled from source, profiled against Node 26.5.1) and
re-measured on Perry 7661bc0 (v0.5.1589), Linux x64. The pre-ES6 object model (function F(){}, F.prototype.m = …, new F(), x instanceof F, new this.constructor(v)) never gets a class id or a cached
prototype: every new F() runs js_new_function_construct (10,464 instructions for an empty constructor vs 625
for new KE() of an empty class) and every x instanceof F runs the dynamic ladder (3,770 vs 185).
Reproduction
fnctor.ts:
// Pre-ES6 object model (decimal.js, dayjs, lodash): `function F(){}` constructors, `x instanceof F`, `new x.constructor(v)`.constvariant=process.argv[2];constN=Number(process.argv[3]);functionE(this: any){}classKE{}functionclone(){// decimal.js: the constructor is a closure made by clone()functionDec(this: any,v: number): any{constx=this;if(!(xinstanceofDec))returnnew(Decasany)(v);x.constructor=Dec;x.d=v;}Dec.prototype.plus=function(this: any,y: number){constCtor=this.constructor;returnnewCtor((this.d+y)%1000003);};returnDec;}constD: any=clone();classC{d: number;constructor(v: number){this.d=v;}plus(y: number){returnnewC((this.d+y)%1000003);}}constfe: any=new(Easany)();constke: any=newKE();constkeep: any[]=newArray(1024);functionrun(n: number): number{lets=0;if(variant==="new_fn_empty")for(leti=0;i<n;i++){keep[i&1023]=new(Easany)();s+=1;}elseif(variant==="new_class_empty")for(leti=0;i<n;i++){keep[i&1023]=newKE();s+=1;}elseif(variant==="instanceof_fn")for(leti=0;i<n;i++)s+=feinstanceofE ? 1 : 0;elseif(variant==="instanceof_class")for(leti=0;i<n;i++)s+=keinstanceofKE ? 1 : 0;elseif(variant==="decimal_fn"){letx=newD(1);for(leti=0;i<n;i++)x=x.plus(i);s=x.d;}elseif(variant==="decimal_class"){letx=newC(1);for(leti=0;i<n;i++)x=x.plus(i);s=x.d;}returns;}run(N/5|0);constt0=performance.now();constcs=run(N);console.log(`variant=${variant} checksum=${cs} ms=${(performance.now()-t0).toFixed(2)}`);
Checksums identical. The empty-constructor and instanceof rows contain no property adds, so they isolate this
mechanism from #10496 (adds never cached), which accounts for ~35 % of the decimal_fn row.
decimal.js 10.6 (26.7× Node overall, objects-group report): js_new_function_construct 8.8 %, instanceof
dynamic tail 15.6 %, plus the property adds and prototype-method calls of the same object model — ≈ 46 % of Perry
time is this model. Every operation runs new Ctor(…) with Ctor = x.constructor and isDecimalInstance(y) (y instanceof Decimal); Decimal is a closure created inside clone().
dayjs (85× Node, strings-group report): the Dayjs constructor chain — the audit's function-constructor
microbenchmark was 141× vs 52× for the same code as a class.
lodash: Stack/MapCache/ListCache/Hash are function constructors (≈ 1 %).
Mechanism
crates/perry-runtime/src/object/class_registry/construct.rs:242js_new_function_construct (verified), per call:
bound-function / boxed-primitive / RegExp / Proxy / non-constructable-builtin checks; is_function_prototype_object_value
(:321, resolves globalThis.Function.prototype by name — see perf: missing-property reads on functions are ~2,600× and Object.hasOwn/getPrototypeOf 40–90× slower than Node (Function.prototype re-resolved by name per call) #10497); bound_native_callable_module_and_method
(:324); identify_global_builtin_constructor (:470); arrow and extends_target_must_throw checks (:1046); synthetic_class_id_for_function (:1049, a hash lookup keyed by the function); allocation; then closure_get_dynamic_prop(fp, "prototype") (:1083) and a static-prototype side-table insert for the instance.
crates/perry-runtime/src/closure/dynamic_props.rs:900closure_get_dynamic_prop (verified): first get_accessor_descriptor(ptr, prop) (:919; when the per-object summary admits a possible entry it looks up (ptr, key.to_string()) — a String allocation + SipHash, crates/perry-runtime/src/object/descriptor_state.rs), then locks the process-global Mutex<PtrHashMap<usize, ClosureProps>> (:94, :936) and hashes the &str name. F.prototype therefore costs up to a
string allocation, two hash lookups and a mutex on every new and every instanceof (inferred from source + profile).
crates/perry-runtime/src/object/instanceof.rs:166js_instanceof_dynamic (verified): Proxy unwrap, Temporal
kind, own Symbol.hasInstance probe (:225), builtin_ctor_class_id_from_value (:127, reads the closure's name dynamic prop and string-matches it, :292), identify_global_builtin_constructor (:481), intl_instanceof (:541), and only then js_instanceof_dynamic_tail (:626), which reads F.prototype from the
closure side table again and walks the chain.
None of these answers is cached per closure, although for an ordinary user function none of them can change
except F.prototype (a write that can bump an epoch).
What fast looks like
Give a user function used with new a per-closure constructor record (synthetic class id, "is ordinary user
function" bit, cached prototype object pointer + epoch) established on first new/instanceof; later calls do a
header check + epoch compare and allocate with the cached prototype. Skip the builtin-identity ladder for closures
whose func_ptr is not one of the builtin thunks (a pointer compare, done once).
Found by the package performance audit (real npm packages compiled from source, profiled against Node 26.5.1) and
re-measured on Perry 7661bc0 (v0.5.1589), Linux x64. The pre-ES6 object model (
function F(){},F.prototype.m = …,new F(),x instanceof F,new this.constructor(v)) never gets a class id or a cachedprototype: every
new F()runsjs_new_function_construct(10,464 instructions for an empty constructor vs 625for
new KE()of an empty class) and everyx instanceof Fruns the dynamic ladder (3,770 vs 185).Reproduction
fnctor.ts:Measurements
Median of 3, N = 1,000,000, shared host (loaded; instruction counts are the load-independent figure).
new E()— emptyfunction E(){}new KE()— emptyclass KE {}(control)fe instanceof E(hit)ke instanceof KE(control)x.plus(i)→instanceof+new this.constructor(v)+ 2 property addsChecksums identical. The empty-constructor and instanceof rows contain no property adds, so they isolate this
mechanism from #10496 (adds never cached), which accounts for ~35 % of the
decimal_fnrow.perf record(inclusive):new_fn_empty:js_new_function_construct91 % →js_get_global_this_builtin_value36 %,closure_get_dynamic_prop17 %,get_accessor_descriptor12 %,identify_global_builtin_constructor12 %,extends_target_must_throw7 %,object_set_static_prototype_impl7 %,synthetic_class_id_for_function5 %,SipHash 4 %.
instanceof_fn:js_instanceof_dynamic68 % →builtin_ctor_class_id_from_value28 %,closure_get_dynamic_prop27 %,
js_instanceof_dynamic_tail20 %,js_object_has_own_symbol(Symbol.hasInstance) 18 %,synthetic_class_id_for_function17 %,identify_global_builtin_constructor15 %,well_known_symbol14 %.decimal_fn:js_new_function_construct79 %,js_put_value_set_ic_miss35 % (the adds),js_instanceof_dynamic9 %.Impact
From the audit profiles (v0.5.1587):
js_new_function_construct8.8 %,instanceofdynamic tail 15.6 %, plus the property adds and prototype-method calls of the same object model — ≈ 46 % of Perry
time is this model. Every operation runs
new Ctor(…)withCtor = x.constructorandisDecimalInstance(y)(y instanceof Decimal);Decimalis a closure created insideclone().Dayjsconstructor chain — the audit's function-constructormicrobenchmark was 141× vs 52× for the same code as a class.
transpileModule, large-group report):js_new_function_construct→replay_class_object_constructor15.2 % inclusive (NodeObject/TokenObject/SymbolObjectconstructors — classexpressions constructed through a variable, which enter the same function; overlaps perf:
newon a class expression is 159× slower than Node vs 7× for the identical declaration (constructorthis.x =fields inferred only for declarations) #10499).Stack/MapCache/ListCache/Hashare function constructors (≈ 1 %).Mechanism
crates/perry-runtime/src/object/class_registry/construct.rs:242js_new_function_construct(verified), per call:bound-function / boxed-primitive / RegExp / Proxy / non-constructable-builtin checks;
is_function_prototype_object_value(
:321, resolvesglobalThis.Function.prototypeby name — see perf: missing-property reads on functions are ~2,600× andObject.hasOwn/getPrototypeOf40–90× slower than Node (Function.prototype re-resolved by name per call) #10497);bound_native_callable_module_and_method(
:324);identify_global_builtin_constructor(:470); arrow andextends_target_must_throwchecks (:1046);synthetic_class_id_for_function(:1049, a hash lookup keyed by the function); allocation; thenclosure_get_dynamic_prop(fp, "prototype")(:1083) and a static-prototype side-table insert for the instance.crates/perry-runtime/src/closure/dynamic_props.rs:900closure_get_dynamic_prop(verified): firstget_accessor_descriptor(ptr, prop)(:919; when the per-object summary admits a possible entry it looks up(ptr, key.to_string())— aStringallocation + SipHash,crates/perry-runtime/src/object/descriptor_state.rs), then locks the process-globalMutex<PtrHashMap<usize, ClosureProps>>(:94,:936) and hashes the&strname.F.prototypetherefore costs up to astring allocation, two hash lookups and a mutex on every
newand everyinstanceof(inferred from source + profile).crates/perry-runtime/src/object/instanceof.rs:166js_instanceof_dynamic(verified): Proxy unwrap, Temporalkind, own
Symbol.hasInstanceprobe (:225),builtin_ctor_class_id_from_value(:127, reads the closure'snamedynamic prop and string-matches it,:292),identify_global_builtin_constructor(:481),intl_instanceof(:541), and only thenjs_instanceof_dynamic_tail(:626), which readsF.prototypefrom theclosure side table again and walks the chain.
except
F.prototype(a write that can bump an epoch).What fast looks like
functionused withnewa per-closure constructor record (synthetic class id, "is ordinary userfunction" bit, cached
prototypeobject pointer + epoch) established on firstnew/instanceof; later calls do aheader check + epoch compare and allocate with the cached prototype. Skip the builtin-identity ladder for closures
whose
func_ptris not one of the builtin thunks (a pointer compare, done once).new_fn_empty≤ 2×new_class_empty(≤ 1,300 instructions),instanceof_fn≤ 2×instanceof_class,decimal_fn≤ 3×decimal_classonce perf: adding a property witho.k = vis ~100× slower than Node (static-key write IC primes only overwrites; no add-transition cache) #10496 and perf: method calls on untyped receivers (prototype methods,fn.call,pushonany) are 44–520× slower than Node (no call-site cache; name re-resolved per call) #10505 are fixed.Notes
closed perf:
new Klass(v,w)is 63% slower than the equivalent object literal (28.5x vs 17.4x Node) #7512).x instanceof Fis always false whenFis an imported non-class constructor (ES5 function, factory-made function, CJSmodule.exports = F); namespace access and local aliases work #10477 (x instanceof Ffalse for imported non-class constructors),Object.create(proto).constructoris a bogus class reference instead ofproto.constructor;ctor instanceof ctorthen segfaults (lodashisEqual(cloneDeep(x), x)) #10478 (Object.create(proto).constructorbogus class reference),argumentsin a class constructor is empty when the class is constructed through a value, and padded to the declared parameter count on a staticnew#10484 (argumentsin a class constructorconstructed through a value). A fix that caches the constructor record should keep those in view.
o.k = vis ~100× slower than Node (static-key write IC primes only overwrites; no add-transition cache) #10496 (property adds in the constructor body), perf: method calls on untyped receivers (prototype methods,fn.call,pushonany) are 44–520× slower than Node (no call-site cache; name re-resolved per call) #10505 (prototype-method calls on theinstances), perf: missing-property reads on functions are ~2,600× and
Object.hasOwn/getPrototypeOf40–90× slower than Node (Function.prototype re-resolved by name per call) #10497 (Function.prototyperesolved by name).