Skip to content

Commit 7841d8b

Browse files
Reuben Brooksclaude
andcommitted
Specialize function wrappers by arity to avoid rest/spread on the hot path
Every Shen-level call went through a variadic (...args) wrapper that allocates a rest array and re-spreads it, purely to support currying. V8 partially optimizes this; JSC materializes the array every call, making Bun ~2x slower than Node on call-heavy code despite comparable raw-recursion speed. funSync/funAsync now emit fixed-parameter wrappers for arities 0-4 (101/102 defuns in a shaken kernel), using arguments.length to detect exact application and falling back to the extracted generic path for partial/over/zero-arg application and arity > 4. Semantics preserved branch-for-branch, including the arity:NaN quirk for partials of raw closures; async wrappers remain AsyncFunction instances for fun's dispatch and js.async?. fib(32) standalone artifact, end-to-end: node 393->182 ms, bun 600->128 ms - bun now matches luajit (~130 ms) on this workload. All suites green: backend 369, kernel 134/134, extensions, ratatoskr e2e, frontend, bun smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 951a2e5 commit 7841d8b

1 file changed

Lines changed: 58 additions & 10 deletions

File tree

lib/runtime.js

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,66 @@ export default (options = {}) => {
9999
|| isArray(x) && isArray(y) && x.length === y.length && x.every((v, i) => equate(v, y[i]))
100100
|| isObject(x) && isObject(y) && equateType(x, y) && Object.keys(x).every(k => equate(x[k], y[k]));
101101

102+
// Generic (rest/spread) paths, only taken for partial application,
103+
// over-application, zero-arg re-wrap, or arities above the specialization
104+
// cutoff. The hot path - calling a wrapper with exactly `arity` arguments -
105+
// goes through the fixed-parameter wrappers below, which avoid
106+
// materializing a rest array on every call (a dominant cost on JSC and a
107+
// measurable one on V8). `args` must be a real array here.
108+
const funSyncGeneric = (f, arity, args) =>
109+
args.length === arity ? f(...args) :
110+
args.length > arity ? bounce(() => asFunction(settle(f(...args.slice(0, arity))))(...args.slice(arity))) :
111+
args.length === 0 ? funSync(f, arity) :
112+
Object.assign(funSync((...more) => f(...args, ...more), arity - args.length), { arity: f.arity - args.length });
113+
const funAsyncGeneric = (f, arity, args) =>
114+
args.length === arity ? f(...args) :
115+
args.length > arity ? bounce(async () => asFunction(await settle(f(...args.slice(0, arity))))(...args.slice(arity))) :
116+
args.length === 0 ? funAsync(f, arity) :
117+
Object.assign(funAsync((...more) => f(...args, ...more), arity - args.length), { arity: f.arity - args.length });
118+
// Arity-specialized wrappers: `function` (not arrow) so `arguments` is
119+
// available to detect exact application without a rest parameter. `this`
120+
// is unused throughout.
121+
const slice = args => Array.prototype.slice.call(args);
122+
const funSyncs = [
123+
f => function () {
124+
return arguments.length === 0 ? f() : funSyncGeneric(f, 0, slice(arguments));
125+
},
126+
f => function (a) {
127+
return arguments.length === 1 ? f(a) : funSyncGeneric(f, 1, slice(arguments));
128+
},
129+
f => function (a, b) {
130+
return arguments.length === 2 ? f(a, b) : funSyncGeneric(f, 2, slice(arguments));
131+
},
132+
f => function (a, b, c) {
133+
return arguments.length === 3 ? f(a, b, c) : funSyncGeneric(f, 3, slice(arguments));
134+
},
135+
f => function (a, b, c, d) {
136+
return arguments.length === 4 ? f(a, b, c, d) : funSyncGeneric(f, 4, slice(arguments));
137+
}
138+
];
139+
const funAsyncs = [
140+
f => async function () {
141+
return arguments.length === 0 ? f() : funAsyncGeneric(f, 0, slice(arguments));
142+
},
143+
f => async function (a) {
144+
return arguments.length === 1 ? f(a) : funAsyncGeneric(f, 1, slice(arguments));
145+
},
146+
f => async function (a, b) {
147+
return arguments.length === 2 ? f(a, b) : funAsyncGeneric(f, 2, slice(arguments));
148+
},
149+
f => async function (a, b, c) {
150+
return arguments.length === 3 ? f(a, b, c) : funAsyncGeneric(f, 3, slice(arguments));
151+
},
152+
f => async function (a, b, c, d) {
153+
return arguments.length === 4 ? f(a, b, c, d) : funAsyncGeneric(f, 4, slice(arguments));
154+
}
155+
];
102156
const funSync = (f, arity) =>
103-
(...args) =>
104-
args.length === arity ? f(...args) :
105-
args.length > arity ? bounce(() => asFunction(settle(f(...args.slice(0, arity))))(...args.slice(arity))) :
106-
args.length === 0 ? funSync(f, arity) :
107-
Object.assign(funSync((...more) => f(...args, ...more), arity - args.length), { arity: f.arity - args.length });
157+
arity < funSyncs.length ? funSyncs[arity](f) :
158+
(...args) => funSyncGeneric(f, arity, args);
108159
const funAsync = (f, arity) =>
109-
async (...args) =>
110-
args.length === arity ? f(...args) :
111-
args.length > arity ? bounce(async () => asFunction(await settle(f(...args.slice(0, arity))))(...args.slice(arity))) :
112-
args.length === 0 ? funAsync(f, arity) :
113-
Object.assign(funAsync((...more) => f(...args, ...more), arity - args.length), { arity: f.arity - args.length });
160+
arity < funAsyncs.length ? funAsyncs[arity](f) :
161+
async (...args) => funAsyncGeneric(f, arity, args);
114162
const fun = (f, arity = f.arity || f.length) =>
115163
Object.assign((f instanceof AsyncFunction ? funAsync : funSync)(f, arity), { arity });
116164

0 commit comments

Comments
 (0)