Skip to content

Optimize Date operations and object resolution#2288

Merged
lahma merged 8 commits into
sebastienros:mainfrom
lahma:stopwatch-perf
Mar 1, 2026
Merged

Optimize Date operations and object resolution#2288
lahma merged 8 commits into
sebastienros:mainfrom
lahma:stopwatch-perf

Conversation

@lahma

@lahma lahma commented Feb 16, 2026

Copy link
Copy Markdown
Collaborator

This PR now includes a broader set of performance optimizations plus correctness fixes after regression triage.

What changed

  • Date/number conversion fast paths
    • TypeConverter.ToNumeric: direct numeric extraction for JsDate
    • DateConstructor.Construct: avoid redundant prototype lookup when newTarget === Date
    • Date constructor cleanup: compute "now" milliseconds once and reuse
  • Object creation fast paths (newTarget === this)
    • Added guarded OrdinaryCreateFromConstructor bypasses in hot built-ins (Array, RegExp, Map, Set, Promise)
  • Execution-path optimizations
    • Engine.GetValue: direct ObjectInstance cast on already-object values
    • Identifier resolution cache in JintIdentifierExpression
    • Direct member-read path in JintMemberExpression
    • Monomorphic own-property cache for stable reads (with safety guard, see below)
    • ScriptFunction base-construction fast path when prototype descriptor is direct data
  • Post-optimization correctness fixes
    • Restricted member-read own-property cache to plain objects to preserve Proxy/exotic object semantics
    • Restored HybridDictionary cutover constants to avoid for-in ordering regressions

Performance comparison vs main

Jint.Benchmark.DromaeoBenchmark

Diff Method Prepared Modern Toolchain Mean Error Allocated
Old CoreEval False False Default 5.098 ms 0.0343 ms 336.25 KB
New False False Default 4.258 ms (-16%) 0.1511 ms 340.11 KB (+1%)
Old CoreEval True False Default 5.911 ms 0.0629 ms 320.93 KB
New True False Default 4.400 ms (-26%) 0.0743 ms 324.46 KB (+1%)
Old CoreEval False True Default 5.120 ms 0.3653 ms 334.68 KB
New False True Default 5.061 ms (-1%) 0.3658 ms 338.54 KB (+1%)
Old CoreEval True True Default 3.896 ms 0.0864 ms 319.9 KB
New True True Default 3.707 ms (-5%) 0.0536 ms 323.43 KB (+1%)
Old Cube False False Default 15.345 ms 0.3024 ms 5750.04 KB
New False False Default 14.982 ms (-2%) 0.2978 ms 5761.48 KB (0%)
Old Cube True False Default 14.438 ms 0.2790 ms 5404.05 KB
New True False Default 13.706 ms (-5%) 0.2718 ms 5408.5 KB (0%)
Old Cube False True Default 15.237 ms 0.3027 ms 5254.75 KB
New False True Default 17.491 ms (+15%) 0.3387 ms 5268.97 KB (0%)
Old Cube True True Default 15.840 ms 0.1616 ms 4909.46 KB
New True True Default 14.180 ms (-10%) 0.2806 ms 4913.91 KB (0%)
Old ObjectArray False False Default 31.516 ms 0.4732 ms 96268.38 KB
New False False Default 30.607 ms (-3%) 0.4960 ms 96269.7 KB (0%)
Old ObjectArray True False Default 34.590 ms 0.6896 ms 96220.13 KB
New True False Default 31.438 ms (-9%) 0.6075 ms 96220.56 KB (0%)
Old ObjectArray False True Default 32.863 ms 0.2809 ms 96266.54 KB
New False True Default 32.343 ms (-2%) 0.6325 ms 96267.85 KB (0%)
Old ObjectArray True True Default 32.960 ms 0.4958 ms 96220.3 KB
New True True Default 33.238 ms (+1%) 0.4552 ms 96220.48 KB (0%)
Old ObjectRegExp False False Default 118.739 ms 2.0258 ms 149567.48 KB
New False False Default 118.387 ms (0%) 2.3578 ms 153475.54 KB (+3%)
Old ObjectRegExp True False Default 87.490 ms 1.4717 ms 146958.28 KB
New True False Default 85.446 ms (-2%) 2.2009 ms 143657.23 KB (-2%)
Old ObjectRegExp False True Default 126.685 ms 2.8042 ms 151435.22 KB
New False True Default 132.438 ms (+5%) 2.9491 ms 148500.75 KB (-2%)
Old ObjectRegExp True True Default 92.044 ms 1.8385 ms 150018.89 KB
New True True Default 82.878 ms (-10%) 1.4041 ms 148156.51 KB (-1%)
Old ObjectString False False Default 169.050 ms 3.3647 ms 1303179.71 KB
New False False Default 168.119 ms (-1%) 3.3389 ms 1303055.45 KB (0%)
Old ObjectString True False Default 164.360 ms 3.5862 ms 1302788.84 KB
New True False Default 153.364 ms (-7%) 3.0500 ms 1302861.69 KB (0%)
Old ObjectString False True Default 174.144 ms 3.4787 ms 1303083.05 KB
New False True Default 187.545 ms (+8%) 6.2599 ms 1303111.8 KB (0%)
Old ObjectString True True Default 163.775 ms 3.2720 ms 1302857.27 KB
New True True Default 151.556 ms (-7%) 3.0238 ms 1302885.52 KB (0%)
Old StringBase64 False False Default 35.009 ms 0.5457 ms 2397.9 KB
New False False Default 36.228 ms (+3%) 0.5524 ms 2399.96 KB (0%)
Old StringBase64 True False Default 32.784 ms 0.6217 ms 2296.68 KB
New True False Default 31.414 ms (-4%) 0.1920 ms 2296.82 KB (0%)
Old StringBase64 False True Default 36.674 ms 0.3183 ms 2397.9 KB
New False True Default 39.851 ms (+9%) 0.7915 ms 2399.36 KB (0%)
Old StringBase64 True True Default 37.721 ms 0.2689 ms 2295.69 KB
New True True Default 37.213 ms (-1%) 0.7283 ms 2296.17 KB (0%)

Jint.Benchmark.StopwatchBenchmark

Diff Method Modern Toolchain Mean Error Allocated
Old Execute False Default 223.4 ms 0.91 ms 59.04 MB
New False Default 215.8 ms (-3%) 3.82 ms 59.04 MB (0%)
Old Execute_ParsedScript False Default 234.6 ms 0.87 ms 59 MB
New False Default 217.7 ms (-7%) 1.38 ms 59 MB (0%)
Old Execute True Default 267.7 ms 1.18 ms 59.25 MB
New True Default 271.1 ms (+1%) 1.38 ms 59.25 MB (0%)
Old Execute_ParsedScript True Default 303.1 ms 0.79 ms 59.21 MB
New True Default 276.6 ms (-9%) 0.89 ms 59.21 MB (0%)

Jint.Benchmark.SunSpiderBenchmark

Diff Method Toolchain FileName Mean Error Allocated
Old Run Default 3d-cube 110.63 ms 1.011 ms 41596.49 KB
New Default 109.17 ms (-1%) 1.069 ms 41609.84 KB (0%)
Old Run Default 3d-morph 101.65 ms 0.680 ms 46251.16 KB
New Default 101.23 ms (0%) 1.223 ms 46252.26 KB (0%)
Old Run Default 3d-raytrace 101.49 ms 0.788 ms 35150.33 KB
New Default 93.50 ms (-8%) 0.304 ms 35167.41 KB (0%)
Old Run Default access-binary-trees 54.99 ms 0.267 ms 43985.47 KB
New Default 51.15 ms (-7%) 0.302 ms 43986.66 KB (0%)
Old Run Default access-fannkuch 279.20 ms 3.815 ms 104.82 KB
New Default 273.96 ms (-2%) 2.193 ms 107.04 KB (+2%)
Old Run Default access-nbody 120.42 ms 0.506 ms 47330.56 KB
New Default 108.72 ms (-10%) 0.617 ms 47335.39 KB (0%)
Old Run Default access-nsieve 73.19 ms 0.434 ms 17141.77 KB
New Default 75.42 ms (+3%) 0.267 ms 17142.53 KB (0%)
Old Run Default bitop(...)-byte [24] 81.63 ms 0.593 ms 26948.13 KB
New Default 80.46 ms (-1%) 0.739 ms 26952.08 KB (0%)
Old Run Default bitops-bits-in-byte 134.16 ms 0.766 ms 16044.45 KB
New Default 124.50 ms (-7%) 0.789 ms 16044.91 KB (0%)
Old Run Default bitops-bitwise-and 80.03 ms 0.574 ms 55938.83 KB
New Default 77.52 ms (-3%) 0.282 ms 55939.02 KB (0%)
Old Run Default bitops-nsieve-bits 125.78 ms 0.684 ms 53916.02 KB
New Default 122.97 ms (-2%) 0.615 ms 53916.93 KB (0%)
Old Run Default contr(...)rsive [21] 64.77 ms 0.261 ms 46291.52 KB
New Default 63.81 ms (-1%) 0.347 ms 46292.29 KB (0%)
Old Run Default crypto-aes 72.43 ms 0.316 ms 6659.25 KB
New Default 72.36 ms (0%) 0.414 ms 6671.09 KB (0%)
Old Run Default crypto-md5 59.23 ms 0.397 ms 42790.9 KB
New Default 57.85 ms (-2%) 0.307 ms 42802.6 KB (0%)
Old Run Default crypto-sha1 62.61 ms 0.585 ms 38048.07 KB
New Default 61.63 ms (-2%) 0.201 ms 38051.4 KB (0%)
Old Run Default date-format-tofte 57.13 ms 0.177 ms 50457.04 KB
New Default 56.60 ms (-1%) 0.296 ms 51028.3 KB (+1%)
Old Run Default date-format-xparb 34.83 ms 0.099 ms 11871.54 KB
New Default 33.50 ms (-4%) 0.167 ms 11874.29 KB (0%)
Old Run Default math-cordic 189.17 ms 0.836 ms 58146.8 KB
New Default 183.21 ms (-3%) 0.461 ms 58148.18 KB (0%)
Old Run Default math-partial-sums 70.21 ms 0.338 ms 49366.16 KB
New Default 72.48 ms (+3%) 0.372 ms 49367.4 KB (0%)
Old Run Default math-spectral-norm 69.11 ms 0.292 ms 29758.97 KB
New Default 70.62 ms (+2%) 0.325 ms 29760.8 KB (0%)
Old Run Default regexp-dna 95.42 ms 0.768 ms 16828.57 KB
New Default 93.33 ms (-2%) 0.560 ms 16829.86 KB (0%)
Old Run Default string-base64 48.89 ms 0.272 ms 3155.7 KB
New Default 49.47 ms (+1%) 0.207 ms 3157.9 KB (0%)
Old Run Default string-fasta 107.88 ms 0.696 ms 107854.73 KB
New Default 110.86 ms (+3%) 0.862 ms 112596.15 KB (+4%)
Old Run Default string-tagcloud 47.39 ms 0.278 ms 28371.25 KB
New Default 45.28 ms (-4%) 0.332 ms 28372.54 KB (0%)
Old Run Default string-unpack-code 46.64 ms 0.255 ms 57495.92 KB
New Default 45.75 ms (-2%) 0.285 ms 57499.28 KB (0%)
Old Run Default strin(...)input [21] 40.16 ms 0.146 ms 16093.63 KB
New Default 41.51 ms (+3%) 0.125 ms 16095.17 KB (0%)

lahma and others added 7 commits March 1, 2026 14:24
…watch benchmark improvement

Three targeted optimizations for Date-heavy workloads:

1. TypeConverter.ToNumeric: Fast path for JsDate objects
   Bypasses the expensive ToPrimitive chain (Symbol.toPrimitive lookup ->
   exotic call -> OrdinaryToPrimitive -> valueOf -> ThisTimeValue) by
   directly returning the date's numeric value. Eliminates ~12 method
   calls per Date-to-number conversion.

2. DateConstructor.Construct: Skip OrdinaryCreateFromConstructor for new Date()
   When newTarget is the built-in DateConstructor (the common case),
   directly construct JsDate with the known prototype instead of going
   through GetPrototypeFromConstructor which does a property lookup.

3. Engine.GetValue: Direct ObjectInstance cast
   When baseValue.IsObject() is already confirmed, use a direct cast
   instead of TypeConverter.ToObject() which performs a redundant type
   check.

StopwatchBenchmark results (ShortRun):

| Variant              | Before   | After    | Improvement |
|----------------------|----------|----------|-------------|
| Classic Execute      | 251.4 ms | 225.3 ms | -10.4%      |
| Classic ParsedScript | 264.3 ms | 231.0 ms | -12.6%      |
| Modern Execute       | 302.6 ms | 271.9 ms | -10.1%      |
| Modern ParsedScript  | 320.2 ms | 277.8 ms | -13.2%      |

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a direct value path in JintMemberExpression.GetValue for common
non-optional obj.prop reads with pre-determined string property names.

When base resolves to ObjectInstance, this bypasses Reference pool rent/
resolve round-trip and reads property directly via ObjectInstance.Get.
Initialization logic is also shared through EnsureInitialized().

StopwatchBenchmark (ShortRun, before vs after this commit):
- Execute (Modern=false): 290.3 ms -> 245.6 ms (-15.4%)
- Execute (Modern=true): 306.9 ms -> 301.9 ms (-1.6%)
- Execute_ParsedScript (Modern=false): 256.2 ms -> 255.0 ms (-0.5%)
- Execute_ParsedScript (Modern=true): 317.6 ms -> 320.5 ms (+0.9%)
- Aggregate of all four means: 1171.0 ms -> 1123.0 ms (-4.1%)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iptor is data

Optimize IConstructor.Construct for ScriptFunction base constructors by
bypassing OrdinaryCreateFromConstructor when all of these hold:
- newTarget is the function itself
- prototype descriptor exists
- prototype descriptor is not an accessor descriptor

In this case we can create JsObject directly and assign prototype from the
data descriptor, avoiding constructor property lookup and helper indirection.
Accessor prototype descriptors still go through the full path to preserve
observable getter semantics.

StopwatchBenchmark (ShortRun, step2 -> step3):
- Execute_ParsedScript (Modern=false): 255.0 -> 223.9 ms (-12.2%)
- Execute_ParsedScript (Modern=true): 320.5 -> 269.2 ms (-16.0%)
- Execute (Modern=false): 245.6 -> 291.0 ms (+18.5%)
- Execute (Modern=true): 301.9 -> 272.4 ms (-9.8%)
- Aggregate of all four means: 1123.0 -> 1056.5 ms (-5.9%)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rExpression

Add a lightweight per-node cache for identifiers that resolve in the current
lexical environment.

For repeated accesses in the same environment and strict-mode context, this:
- skips full scope-chain walks in EvaluateInternal
- skips full TryGetIdentifierEnvironmentWithBindingValue in GetValue
- falls back to full lookup when environment or strictness differs

Cache is deliberately scoped to current-environment bindings to preserve
correctness under shadowing and dynamic scope changes.

StopwatchBenchmark (ShortRun, step3 -> step4):
- Execute (Modern=false): 291.0 -> 222.6 ms (-23.5%)
- Execute (Modern=true): 272.4 -> 267.7 ms (-1.7%)
- Execute_ParsedScript (Modern=false): 223.9 -> 221.2 ms (-1.2%)
- Execute_ParsedScript (Modern=true): 269.2 -> 281.3 ms (+4.5%)
- Aggregate of all four means: 1056.5 -> 992.8 ms (-6.0%)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend JintMemberExpression direct read path with a small monomorphic cache:
- cache (object instance, property descriptor) for non-configurable own props
- on cache hit, unwrap cached descriptor directly
- on miss, fallback to normal own-property/prototype path

Caching is intentionally restricted to non-configurable own properties to keep
semantics safe under delete/reconfigure operations.

StopwatchBenchmark (ShortRun, step4 -> step5):
- Execute (Modern=false): 222.6 -> 216.9 ms (-2.6%)
- Execute (Modern=true): 267.7 -> 274.3 ms (+2.5%)
- Execute_ParsedScript (Modern=false): 221.2 -> 228.0 ms (+3.1%)
- Execute_ParsedScript (Modern=true): 281.3 -> 272.7 ms (-3.1%)
- Aggregate of all four means: 992.8 -> 991.9 ms (-0.1%)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reduce HybridDictionary list-to-dictionary cutover from 9 to 6 entries so 6-property objects switch to hash lookups sooner, targeting stopwatch member-access hotspots.

Stopwatch Benchmark (default job, A/B average across two runs): -2.65% total mean.

Dromaeo total: +0.58% (noise-level). SunSpider total: -1.40%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restrict the own-property member-read inline cache to plain objects only so Proxy and other exotic objects still go through the full [[Get]] semantics.

Restore HybridDictionary cutover constants back to 9 to avoid dictionary-backed enumeration order regressions in for-in tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lahma
lahma marked this pull request as ready for review March 1, 2026 17:15
@lahma
lahma enabled auto-merge (squash) March 1, 2026 17:15
@lahma
lahma merged commit 832f822 into sebastienros:main Mar 1, 2026
6 of 8 checks passed
@lahma
lahma deleted the stopwatch-perf branch March 1, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant