-
-
Notifications
You must be signed in to change notification settings - Fork 164
perf: don't unroll loops whose body contains a function literal (method slice B1) #11679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| ### Performance | ||
|
|
||
| The static loop unroller no longer clones a loop body that contains a function | ||
| literal (a closure, an arrow, an object-literal method or a class expression). | ||
| Each copy used to become its own compiled function, so objects built in a short | ||
| counted loop such as `for (let i = 0; i < 8; i++) objs.push({ m() { ... } })` | ||
| carried eight different code pointers for one source method, and a method call | ||
| site over them primed past its ways and went megamorphic. One source function | ||
| literal is now one function, and the site keeps a single entry. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| ### Tests | ||
|
|
||
| Two node-parity tests pin the receiver a method body sees: sloppy `this` is | ||
| bound once per activation (one wrapper for a primitive, `globalThis` for | ||
| nullish, objects unchanged), and object-literal and prototype methods called | ||
| through a method site get their receiver on every route (nested calls, arrows, | ||
| throws, `call`/`apply`, getters, constructors, generators and async methods). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Object-literal and prototype methods called through a method site receive | ||
| // their receiver as `this`: nested method calls, arrows inheriting `this`, a | ||
| // throw through a method, extra/missing arguments, `arguments`, the generic | ||
| // path (call/apply/detached), an inherited method, a getter, a function | ||
| // expression called and constructed, generator/async methods, and a strict | ||
| // function given a primitive. Output must match node. | ||
| const N = process.argv.length > 99 ? 1 : 3000; | ||
| function plainFn(this: any) { return this === globalThis; } | ||
| function mk(k: number): any { | ||
| return { | ||
| k, | ||
| m(x: number) { return x + this.k; }, | ||
| outer(x: number) { const r = this.inner(x); return r + this.k; }, | ||
| inner(x: number) { return x * 2 + this.k; }, | ||
| thrower(x: number) { if (x < 0) throw new Error("neg" + this.k); return this.k; }, | ||
| wrap(x: number) { try { this.thrower(-x); } catch (e) { return this.k * 100; } return this.k; }, | ||
| arrow(x: number) { const f = () => this.k + x; return f(); }, | ||
| plain(x: number) { return plainFn() ? x + this.k : -1; }, | ||
| many(a: number, b: number, c: number, d: number) { return this.k + (b === undefined ? 0 : b) + (d === undefined ? 7 : d); }, | ||
| args(x: number) { return arguments.length + this.k; }, | ||
| }; | ||
| } | ||
| // Generator and async methods live on their own literal. | ||
| function mkg(k: number): any { | ||
| return { k, *gen() { yield this.k; yield this.k + 1; }, async am() { await null; return this.k * 3; } }; | ||
| } | ||
| const a = mk(1); | ||
| const b = mk(2); | ||
| let s = 0, t = 0, v = 0, w = 0, y = 0, z = 0, q = 0, caught = 0; | ||
| for (let i = 0; i < N; i++) { | ||
| const o = (i & 1) ? a : b; | ||
| s += o.m(i); | ||
| t += o.outer(i); | ||
| v += o.arrow(i); | ||
| w += o.plain(i); | ||
| y += o.wrap(i + 1); | ||
| z += o.many(i, 1); | ||
| q += o.args(i, i); | ||
| try { o.thrower(i % 7 === 0 ? -1 : i); } catch (e) { caught++; } | ||
| } | ||
| console.log(s, t, v, w, y, z, q, caught); | ||
| // The generic path (no site): call/apply/detached, through the public body. | ||
| const f = a.m; | ||
| console.log(a.m.call(b, 10), a.m.apply({ k: 40 }, [2]), f.call({ k: 7 }, 1), a.outer.call(b, 3)); | ||
| // An inherited method through a site. | ||
| const proto: any = { m(x: number) { return x * 3 + this.k; } }; | ||
| const kids: any[] = []; | ||
| for (let j = 0; j < 4; j++) { const c = Object.create(proto); c.k = j; kids.push(c); } | ||
| let r = 0; | ||
| for (let i = 0; i < N; i++) r += kids[i & 3].m(i); | ||
| console.log(r); | ||
| // A getter, and a function expression stored as a method, called and constructed. | ||
| const g: any = { k: 3, get dbl() { return this.k * 2; } }; | ||
| let gs = 0; | ||
| for (let i = 0; i < N; i++) gs += g.dbl; | ||
| const holder: any = { k: 9 }; | ||
| holder.F = function (this: any, x: number) { this.x = x; return this; }; | ||
| let fs = 0; | ||
| for (let i = 0; i < N; i++) fs += holder.F(i).k; | ||
| const made = new holder.F(4); | ||
| console.log(gs, fs, holder.x, made.x, made === holder, made instanceof holder.F); | ||
| // Generator and async methods keep their receiver. | ||
| const ga = mkg(1); | ||
| const gb = mkg(2); | ||
| console.log(JSON.stringify([...ga.gen()]), JSON.stringify([...gb.gen()])); | ||
| ga.am().then((x: number) => console.log("async", x)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Sloppy-mode `this` is bound ONCE per activation (OrdinaryCallBindThis): | ||
| // undefined/null become globalThis, a primitive becomes ONE wrapper object, | ||
| // objects pass unchanged. Every `this` in one activation — and every arrow | ||
| // that inherits it — must name that same value: `this === this` holds for a | ||
| // primitive receiver. Covers function declarations, nested declarations, | ||
| // function expressions, object-literal methods, generators, async functions, | ||
| // class references, and a "use strict" function in a sloppy file. | ||
| function kind(this: any) { return typeof this; } | ||
| function same(this: any) { return this === this; } | ||
| function viaArrow(this: any) { const a = () => this; return a() === this && a() === a(); } | ||
| function tagOf(this: any) { return Object.prototype.toString.call(this); } | ||
| function isGlobal(this: any) { return this === globalThis; } | ||
| function keep(this: any) { return this; } | ||
| function twice(this: any) { const x = this; const y = this; return x === y; } | ||
| function mutate(this: any) { this.extra = 1; return this.extra; } | ||
|
|
||
| const receivers: any[] = [1, 2.5, -0, NaN, true, false, "s", "", 10n]; | ||
| for (const r of receivers) { | ||
| const label = typeof r === "bigint" ? "bigint" : JSON.stringify(r); | ||
| console.log( | ||
| "decl", label, kind.call(r), same.call(r), viaArrow.call(r), tagOf.call(r), | ||
| twice.call(r), mutate.call(r), keep.call(r) === keep.call(r), | ||
| ); | ||
| } | ||
| console.log("nullish", isGlobal.call(undefined), isGlobal.call(null), isGlobal(), kind.call(undefined)); | ||
| const o = { a: 1 }; | ||
| console.log("object", keep.call(o) === o, same.call(o), kind.call(o)); | ||
| const fnRecv = function () { return 3; }; | ||
| console.log("function", keep.call(fnRecv) === fnRecv, kind.call(fnRecv)); | ||
|
|
||
| // Nested declaration and a function expression. | ||
| function outer(this: any) { | ||
| function inner(this: any) { return [typeof this, this === this, this instanceof Number]; } | ||
| return inner.call(this); | ||
| } | ||
| console.log("nested", JSON.stringify(outer.call(7)), JSON.stringify(outer.call(o))); | ||
| const expr = function (this: any) { return [typeof this, this === this, this instanceof String]; }; | ||
| console.log("expr", JSON.stringify(expr.call("x")), JSON.stringify(expr.call(4))); | ||
|
|
||
| // Object-literal method called with a primitive. | ||
| const lit: any = { m() { return [typeof this, this === this, this instanceof Boolean]; } }; | ||
| console.log("literal", JSON.stringify(lit.m.call(true)), JSON.stringify(lit.m.call(9))); | ||
|
|
||
| // A method call ON a primitive: the receiver reaches the body unboxed, and | ||
| // the body binds it once. | ||
| (Number.prototype as any).kindOf = function (this: any) { return [typeof this, this === this, this instanceof Number]; }; | ||
| (String.prototype as any).kindOf = function (this: any) { const a = () => this; return [typeof this, this === a(), this.length]; }; | ||
| (Boolean.prototype as any).kindOf = function (this: any) { return [typeof this, this === this, this.valueOf()]; }; | ||
| const five: any = 5; | ||
| const str: any = "abc"; | ||
| const yes: any = true; | ||
| console.log("proto-method", JSON.stringify(five.kindOf()), JSON.stringify(str.kindOf()), JSON.stringify(yes.kindOf())); | ||
| let hot = 0; | ||
| for (let i = 0; i < 3000; i++) { const r = (i as any).kindOf(); if (r[0] === "object" && r[1] && r[2]) hot++; } | ||
| console.log("proto-method-hot", hot); | ||
|
|
||
| // A detached function expression / method called plainly binds globalThis. | ||
| const detachedExpr = function (this: any) { return this === globalThis; }; | ||
| const detachedLit: any = { m() { return this === globalThis; } }; | ||
| const dm = detachedLit.m; | ||
| console.log("detached", detachedExpr(), dm(), typeof (function (this: any) { return this; })()); | ||
|
|
||
| // A primitive receiver's wrapper is a fresh object per ACTIVATION. | ||
| console.log("per-activation", keep.call(5) === keep.call(5), keep.call(5) == keep.call(5)); | ||
| console.log("value", keep.call(5).valueOf(), keep.call("ab").length, keep.call(true).valueOf()); | ||
|
|
||
| // Class reference receiver stays the class. | ||
| class C { static tag = "C"; } | ||
| function readTag(this: any) { return this === C ? this.tag : "boxed"; } | ||
| console.log("classref", readTag.call(C), keep.call(C) === C); | ||
|
|
||
| // Generators and async functions bind their receiver the same way. | ||
| function* gen(this: any) { yield typeof this; yield this === this; const a = () => this; yield a() === this; } | ||
| console.log("generator", JSON.stringify([...gen.call(3)]), JSON.stringify([...gen.call("q")])); | ||
| async function af(this: any) { const before = this; await null; return [typeof this, this === before, this === this]; } | ||
| af.call(8).then((v) => console.log("async", JSON.stringify(v))); | ||
|
|
||
| // A "use strict" function in a sloppy file sees the primitive itself. | ||
| function strictOne(this: any) { "use strict"; return [typeof this, this === this, this === 6]; } | ||
| function strictKeep(this: any) { "use strict"; return this; } | ||
| console.log("strict", JSON.stringify(strictOne.call(6)), strictKeep.call(undefined) === undefined, strictKeep.call("z") === "z"); | ||
|
|
||
| // Many activations under allocation pressure keep their own wrappers. | ||
| let ok = 0; | ||
| for (let i = 0; i < 20000; i++) { | ||
| const w = keep.call(i); | ||
| const junk = { i, s: "x" + i }; | ||
| if (typeof w === "object" && w.valueOf() === i && same.call(i) && junk.i === i) ok++; | ||
| } | ||
| console.log("pressure", ok); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 8127
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 21684
Rename the fixture to
.cts.The runner loads plain
.tsfiles as strict ESM. Therefore,mutate.call(r)can throw whenris a primitive, and the other functions do not test sloppythissubstitution or boxing. The CommonJS retry does not apply to this fixture.Rename the file so Node and Perry both use CommonJS script semantics.
Suggested fix
🧰 Tools
🪛 Biome (2.5.12)
[error] 9-9: This comparison uses the same expression on both sides.
(lint/suspicious/noSelfCompare)
[error] 10-10: This comparison uses the same expression on both sides.
(lint/suspicious/noSelfCompare)
🤖 Prompt for AI Agents