Skip to content

Optimize parser and tokenizer hot paths#30

Merged
adams85 merged 3 commits into
adams85:masterfrom
lahma:perf/optimize-hot-paths
Mar 8, 2026
Merged

Optimize parser and tokenizer hot paths#30
adams85 merged 3 commits into
adams85:masterfrom
lahma:perf/optimize-hot-paths

Conversation

@lahma

@lahma lahma commented Mar 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Performance optimization targeting tokenizer and parser hot paths, addressing issue #10. Applies AggressiveOptimization and AggressiveInlining JIT hints to high-frequency methods and eliminates redundant branching in ParseClassElement.

Changes

1. AggressiveOptimization on Tokenizer Hot Paths (Tokenizer.cs)

These methods execute on every single token (~34K-68K calls for the largest benchmark files):

  • TryReadToken — Core token dispatch switch (~20 cases). AggressiveOptimization helps the JIT produce better jump tables for the large switch.
  • SkipSpace — Called before every token. Tight character loop with GetCharFlags lookup table. The JIT can better optimize the inner loop and remove redundant bounds checks.
  • ReadWord1 — Identifier character reading loop, called for every identifier/keyword. Tight loop over FullCharCodeAtPosition() + IsIdentifierChar().
  • ReadWord — Called after ReadWord1 for keyword lookup + string deduplication. On every identifier path.

2. AggressiveOptimization on Parser Hot Paths (Parser.Expression.cs, Parser.Statement.cs)

  • ParseStatement — Called for every statement. Contains a large keyword switch dispatch — the most frequently called parser method.
  • ParseSubscripts — Loop method over the already-optimized ParseSubscript. On the expression hot path for every member access, call, and optional chain.
  • ParseMaybeConditional — Direct hot path between ParseMaybeAssign (already optimized) and ParseMaybeBinary. Small but very frequently called.
  • ParseMaybeBinary — Entry point to binary operator parsing. Bridges to ParseBinaryOp (already optimized) and ParseMaybeUnary (already optimized).

3. AggressiveInlining on Small Utility Methods (Parser.ParseUtil.cs)

  • CanInsertSemicolon — Called 12+ times across the parser. Body is just 3 field comparisons (~12 bytes native). Eliminates call overhead and enables the JIT to fold conditions into callers.
  • IsContextual — Called 14+ times. Body is 3 field reads + comparisons. Already small enough for JIT auto-inlining on .NET Core, but the attribute guarantees it on .NET Framework too.

4. ParseClassElement goto Optimization (Parser.Statement.cs)

Eliminated redundant branching in ParseClassElement by using goto ParseElementName to skip checks that are known to be unnecessary once a keyword (static, async, get, set, accessor) is determined not to be a modifier.

Before: When e.g. static is consumed but isn't a keyword (line 1378), keyName = "static" is set, then the code still evaluates keyName is null checks before isAsync, isGenerator, get/set/accessor checks, all of which are guaranteed false.

After: Each fallthrough path jumps directly to element name parsing, skipping ~4-5 redundant null checks and EatContextual calls that can never succeed.

Variables isAsync, isGenerator, isAccessor, kind are moved to the top with default values (false/Unknown) so jumping over their previous declaration sites is safe.

5. Benchmark: Add angular-1.7.9 (FileParsingBenchmark.cs)

Added angular-1.7.9.js (1.3MB, 71 class definitions) to the benchmark suite. The existing 7 files are all pre-ES6 and contain no class syntax, making the ParseClassElement goto optimization unmeasurable. This modern file provides coverage for ES6+ code paths.

Benchmark Results

Environment: AMD Ryzen 9 5950X, Windows 11, BenchmarkDotNet v0.15.6

.NET 10.0

File Baseline (μs) Optimized (μs) Change
backbone-1.1.0 841 830 -1.3%
underscore-1.5.2 712 692 -2.9%
mootools-1.4.5 4,038 3,883 -3.8%
jquery-1.9.1 4,996 4,923 -1.5%
jquery.mobile-1.4.2 8,668 8,253 -4.8%
yui-3.12.0 3,511 3,522 +0.3% (noise)

.NET Framework 4.8

File Baseline (μs) Optimized (μs) Change
backbone-1.1.0 2,216 2,142 -3.3%
underscore-1.5.2 1,922 1,848 -3.8%
mootools-1.4.5 10,226 10,043 -1.8%
jquery-1.9.1 12,761 12,570 -1.5%
jquery.mobile-1.4.2 21,278 20,484 -3.7%
yui-3.12.0 9,749 9,144 -6.2%
angular-1.2.5 16,468 16,069 -2.4%

Key observations:

  • net48 benefits more (1.5-6.2%) than net10.0 (1.3-4.8%), as expected — the legacy JIT relies more heavily on explicit optimization hints since it lacks some of the modern JIT's auto-tuning.
  • Zero allocation change across all files on both frameworks — these are pure throughput improvements with no GC impact.
  • The angular-1.2.5 baseline on net10.0 had a bimodal distribution warning and is excluded from the net10.0 table above.

Test plan

  • Full test suite passes: 10,794 tests on net9.0, 10,901 on net10.0, 10,794 on net8.0, 10,788 on net462
  • Zero allocation regressions in benchmarks
  • Benchmarked on both .NET 10.0 and .NET Framework 4.8
  • All changes are JIT hint annotations or control flow restructuring — no semantic changes to parsing logic

🤖 Generated with Claude Code

Apply AggressiveOptimization/AggressiveInlining hints to high-frequency
methods and eliminate redundant branching in ParseClassElement using goto.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR applies JIT optimization hints (AggressiveOptimization and AggressiveInlining) to tokenizer and parser hot paths, and eliminates redundant branching in ParseClassElement using goto labels. It also adds angular-1.7.9 to the benchmark suite to measure improvements on ES6+ code paths. These are pure throughput improvements targeting both .NET and .NET Framework runtimes.

Changes:

  • Added AggressiveOptimization to tokenizer hot paths (TryReadToken, SkipSpace, ReadWord1, ReadWord) and parser hot paths (ParseStatement, ParseSubscripts, ParseMaybeConditional, ParseMaybeBinary).
  • Added AggressiveInlining to CanInsertSemicolon and IsContextual utility methods.
  • Refactored ParseClassElement to use goto ParseElementName to skip redundant null-checks and contextual-keyword Eat calls after determining a keyword is not acting as a modifier.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Acornima/Tokenizer.cs Adds AggressiveOptimization to 4 core tokenization hot-path methods
src/Acornima/Parser.Statement.cs Adds AggressiveOptimization to ParseStatement; refactors ParseClassElement with goto to eliminate redundant branching
src/Acornima/Parser.ParseUtil.cs Adds AggressiveInlining to IsContextual and CanInsertSemicolon utility methods
src/Acornima/Parser.Expression.cs Adds AggressiveOptimization to 4 expression-parsing hot-path methods
benchmarks/Acornima.Benchmarks/FileParsingBenchmark.cs Adds angular-1.7.9 benchmark to cover ES6+ code paths

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@adams85

adams85 commented Mar 8, 2026

Copy link
Copy Markdown
Owner

I knew there was something more there :)

Getting 2-6% perf improvement by sprinkling some attributes here and there -> mind blown 🤯

Plus, this little change bumping the max. recursion depth from 610 back to 900 is just the icing on the cake!

Big thanks!

@adams85
adams85 force-pushed the perf/optimize-hot-paths branch from d1b48ec to 95d401e Compare March 8, 2026 21:46
@adams85
adams85 force-pushed the perf/optimize-hot-paths branch from 95d401e to cc1afbb Compare March 8, 2026 21:50
@adams85 adams85 linked an issue Mar 8, 2026 that may be closed by this pull request
3 tasks
@adams85
adams85 merged commit 687dbcb into adams85:master Mar 8, 2026
3 checks passed
@lahma
lahma deleted the perf/optimize-hot-paths branch March 9, 2026 06:32
@adams85

adams85 commented Mar 10, 2026

Copy link
Copy Markdown
Owner

@lahma FYI, I had to revert most of the changes from this PR, as it turned out that not only didn't they improve the performance but caused 5-6% degradation, at least on my machine. I have no idea what's behind the difference. Maybe it's the hardware?

Anyway, I spent a few hours doing benchmarks, and this was the best configuration I could come up with: d02b931

BTW, I also released the recent batch: https://github.com/adams85/acornima/releases/tag/v1.3.0

@lahma

lahma commented Mar 11, 2026

Copy link
Copy Markdown
Collaborator Author

@lahma FYI, I had to revert most of the changes from this PR, as it turned out that not only didn't they improve the performance but caused 5-6% degradation, at least on my machine. I have no idea what's behind the difference. Maybe it's the hardware?

Anyway, I spent a few hours doing benchmarks, and this was the best configuration I could come up with: d02b931

Thanks for the feedback, I need to rerun some tests evidently... I'm running on Ryzen 9 5950X so that might also introduce some differences between our setups.

BTW, I also released the recent batch: v1.3.0 (release)

Got that updated and 128 more tests passing again, yay 🚀 sebastienros/jint#2322

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.

Further performance optimization

3 participants