Perf #862: promote non-escaping object literals to generated shape structs#867
Merged
Conversation
…ructs
A compiled object literal compiled to a Dictionary<string,object>: each field
write a string-keyed set_Item with the value boxed, each read an
isinst Dictionary -> TryGetValue -> ConvertToNumber. On the objects benchmark
({ x:i, y:i+1 }) that is ~14x slower than Node despite the shape and field types
being statically known.
A provably non-escaping const/let object literal with a fixed primitive shape is
now promoted to a generated value-type $Shape_N struct with typed fields
(number->double, boolean->bool, string->string). o.x reads/writes lower to direct
ldfld/stfld -- no Dictionary, no string hash, no boxing -- and a non-escaping
struct local is register-promoted by the JIT, so the hot loop becomes pure
unboxed double arithmetic. objectWork(20M): ~747ms -> ~25ms (~31x), identical
output; the loop emits zero Dictionary/box/GetProperty and IL-verifies.
Third instance of the #857/#858 conservative non-escaping-local promotion
pattern, and fully additive: any escape (passed/returned/spread/===/captured/
o[expr]/compound-assign/enumerated/non-primitive or undefined-admitting field)
disqualifies the local and falls back to the existing Dictionary path, so the
object-semantics surface (descriptors, freeze, Object.keys, spread, delete) is
untouched.
- ObjectLocalPromotionAnalyzer: whole-program escape analysis (mirrors
ArrayLocalPromotionAnalyzer / NonEscapingArrowLocalAnalyzer).
- TypeMap carries the pure-data ObjectShapeInfo; ObjectShapeRegistry holds the
generated TypeBuilders, threaded through the CompilationContext construction
sites like DisplayClasses / DirectCallArrowBindings.
- DefineObjectShapeTypes emits one $Shape_N per distinct shape (BCL-only fields,
so standalone output is preserved); declaration + property get/set fast paths
in ILEmitter. The object get/set fast paths precede the TypeInfo.Record
Dictionary path (a promoted local is Record-typed but its slot is a struct).
Compound member-assign (o.x += v) and async/generator-body locals are deferred
(they fall back). +17 dual-mode tests; Object/Record/Property/Freeze/Spread/
ForIn, Compiler/Closure/Loop, Generator/Async, Module/Import/Standalone green.
Resolve ILEmitter.Statements.cs EmitVarDeclaration conflict with the #858 follow-up (non-capturing local-arrow direct static call): keep both mutually-exclusive promotion branches — the non-capturing arrow branch (its comment refers to the capturing branch above) followed by the #862 object-literal shape-struct branch, each before the typed-array branch.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Closes #862 (perf epic #856 child). In compiled mode an object literal compiled to a
Dictionary<string,object>— each field write a string-keyedset_Itemwith the value boxed, each read anisinst Dictionary -> TryGetValue -> ConvertToNumber. On theobjectsbenchmark ({ x:i, y:i+1 }) that was ~14x slower than Node even though the shape and field types are statically known.A provably non-escaping
const/letobject literal with a fixed primitive shape is now promoted to a generated value-type$Shape_Nstruct with typed fields (number->double,boolean->bool,string->string).o.xreads/writes lower to directldfld/stfld— no Dictionary, no string hash, no boxing — and a non-escaping struct local is register-promoted by the JIT, so the hot loop becomes pure unboxeddoublearithmetic.This is the third instance of the #857/#858 conservative non-escaping-local promotion pattern, and is fully additive: any escape (passed / returned / spread /
===/ captured /o[expr]/ compound-assign / enumerated / non-primitive orundefined-admitting field) disqualifies the local and falls back to the existing Dictionary path, so the object-semantics surface (descriptors, freeze,Object.keys, spread,delete) is untouched.Performance
Best-of-5, warmed,
objectWork(20,000,000), identical checksum (4e14):Dictionary<string,object>)$Shapestruct)~31x faster on the exact issue workload. The loop emits zero
Dictionary/box/GetPropertyand IL-verifies; the codegen class now matchesfactorial(pure unboxed double), which already beats Node — so compiledobjectsshould meet/exceed Node, the epic goal.How it works
ObjectLocalPromotionAnalyzer— whole-program escape analysis (mirrorsArrayLocalPromotionAnalyzer/NonEscapingArrowLocalAnalyzer): candidate =const/letdeclared once, simple object literal (plainIdentifierKey: value, unique keys, every field a primitive), used only aso.KEYread/write; not captured.TypeMapcarries the pure-dataObjectShapeInfo;ObjectShapeRegistryholds the generatedTypeBuilders, threaded through theCompilationContextconstruction sites likeDisplayClasses/DirectCallArrowBindings.DefineObjectShapeTypesemits one$Shape_Nper distinct shape (BCL-only fields, so standalone output is preserved — no SharpTS.dll dependency); declaration + property get/set fast paths inILEmitter. The object get/set fast paths precede theTypeInfo.RecordDictionary path (a promoted local isRecord-typed but its slot is a struct).o.x += v), async/generator-body locals, nested literals, the escaping reference-POCO tier.Testing
objectWorkis a pure$Shape_0struct (0 Dictionary/box/GetProperty), IL-verifies, correct output; standalone DLL runs with no SharpTS.dll present.SharpTS.Tests/SharedTests/ObjectLocalPromotionTests.cs) covering promotion + the full escape matrix.Interpreter and type-checker are untouched, so Test262-interpreter and TypeScript-conformance results are unaffected by this change. (Note: the committed Test262 baselines are pre-existing stale/flaky on
mainand the in-process compiled Test262 run exhausts the test host — unrelated to this PR.)