From 120b75b3e4c8b054ae568d3f9ee8c17d7d9b4cc9 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 8 Mar 2026 19:49:14 +0200 Subject: [PATCH 1/3] Optimize parser and tokenizer hot paths for ~2-6% throughput improvement Apply AggressiveOptimization/AggressiveInlining hints to high-frequency methods and eliminate redundant branching in ParseClassElement using goto. Co-Authored-By: Claude Opus 4.6 --- .../FileParsingBenchmark.cs | 3 ++- src/Acornima/Parser.Expression.cs | 3 +++ src/Acornima/Parser.ParseUtil.cs | 2 ++ src/Acornima/Parser.Statement.cs | 22 +++++++++++++------ src/Acornima/Tokenizer.cs | 4 ++++ 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/benchmarks/Acornima.Benchmarks/FileParsingBenchmark.cs b/benchmarks/Acornima.Benchmarks/FileParsingBenchmark.cs index 27508962..b605817f 100644 --- a/benchmarks/Acornima.Benchmarks/FileParsingBenchmark.cs +++ b/benchmarks/Acornima.Benchmarks/FileParsingBenchmark.cs @@ -17,7 +17,8 @@ public class FileParsingBenchmark { "jquery-1.9.1", null! }, { "yui-3.12.0", null! }, { "jquery.mobile-1.4.2", null! }, - { "angular-1.2.5", null! } + { "angular-1.2.5", null! }, + { "angular-1.7.9", null! } }; private Parser _acornimaParser = null!; diff --git a/src/Acornima/Parser.Expression.cs b/src/Acornima/Parser.Expression.cs index 77e8a3e9..a96aedd5 100644 --- a/src/Acornima/Parser.Expression.cs +++ b/src/Acornima/Parser.Expression.cs @@ -253,6 +253,7 @@ internal Expression ParseMaybeAssign(ref DestructuringErrors destructuringErrors } // Parse a ternary conditional (`?:`) operator. + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private Expression ParseMaybeConditional(ref DestructuringErrors destructuringErrors, ExpressionContext context) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/expression.js > `pp.parseMaybeConditional = function` @@ -279,6 +280,7 @@ private Expression ParseMaybeConditional(ref DestructuringErrors destructuringEr } // Start the precedence parser. + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private Expression ParseMaybeBinary(ref DestructuringErrors destructuringErrors, ExpressionContext context) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/expression.js > `pp.parseExprOps = function` @@ -595,6 +597,7 @@ private Expression ParseExprSubscripts(ref DestructuringErrors destructuringErro return result; } + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private Expression ParseSubscripts(in Marker startMarker, Expression baseExpr, bool noCalls, ExpressionContext context) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/expression.js > `pp.parseSubscripts = function` diff --git a/src/Acornima/Parser.ParseUtil.cs b/src/Acornima/Parser.ParseUtil.cs index 9b5fb3fe..c8f4b335 100644 --- a/src/Acornima/Parser.ParseUtil.cs +++ b/src/Acornima/Parser.ParseUtil.cs @@ -28,6 +28,7 @@ internal bool Eat(TokenType type) } // Tests whether parsed token is a contextual keyword. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsContextual(string name) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/parseutil.js > `pp.isContextual = function` @@ -65,6 +66,7 @@ private void ExpectContextual(string name) } // Test whether a semicolon can be inserted at the current position. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool CanInsertSemicolon() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/parseutil.js > `pp.canInsertSemicolon = function` diff --git a/src/Acornima/Parser.Statement.cs b/src/Acornima/Parser.Statement.cs index 95b948bd..f0e931a8 100644 --- a/src/Acornima/Parser.Statement.cs +++ b/src/Acornima/Parser.Statement.cs @@ -198,6 +198,7 @@ private bool IsUsingKeyword(bool isFor, out VariableDeclarationKind kind) // regular expression literal. This is to handle cases like // `if (foo) /blah/.exec(foo)`, where looking at the previous token // does not help. + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private Statement ParseStatement(StatementContext context, bool topLevel = false) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/statement.js > `pp.parseStatement = function` @@ -1357,6 +1358,10 @@ private Node ParseClassElement(bool constructorAllowsSuper) string? keyName = null; string keyword; + bool isAsync = false; + bool isGenerator = false; + bool isAccessor = false; + var kind = PropertyKind.Unknown; var isStatic = EatContextual(keyword = "static"); if (isStatic) @@ -1376,29 +1381,28 @@ private Node ParseClassElement(bool constructorAllowsSuper) { isStatic = false; keyName = keyword; + goto ParseElementName; } } - var isAsync = keyName is null && _tokenizerOptions._ecmaVersion >= EcmaVersion.ES8 && EatContextual(keyword = "async"); + isAsync = _tokenizerOptions._ecmaVersion >= EcmaVersion.ES8 && EatContextual(keyword = "async"); if (isAsync) { if (!((IsClassElementNameStart() || _tokenizer._type == TokenType.Star) && !CanInsertSemicolon())) { isAsync = false; keyName = keyword; + goto ParseElementName; } } - var isGenerator = keyName is null + isGenerator = // NOTE: We only need to check the ECMA version in the case of async generators // since non-async generators were introduced in ES6, together with classes. - && (!isAsync || _tokenizerOptions._ecmaVersion >= EcmaVersion.ES9) + (!isAsync || _tokenizerOptions._ecmaVersion >= EcmaVersion.ES9) && Eat(TokenType.Star); - var isAccessor = false; - var kind = PropertyKind.Unknown; - - if (keyName is null && !isAsync && !isGenerator) + if (!isAsync && !isGenerator) { if (EatContextual(keyword = "get")) { @@ -1409,6 +1413,7 @@ private Node ParseClassElement(bool constructorAllowsSuper) else { keyName = keyword; + goto ParseElementName; } } else if (EatContextual(keyword = "set")) @@ -1420,6 +1425,7 @@ private Node ParseClassElement(bool constructorAllowsSuper) else { keyName = keyword; + goto ParseElementName; } } else if (_tokenizerOptions.AllowDecorators() && EatContextual(keyword = "accessor")) @@ -1431,11 +1437,13 @@ private Node ParseClassElement(bool constructorAllowsSuper) else { keyName = keyword; + goto ParseElementName; } } } // Parse element name + ParseElementName: Expression key; bool computed; if (keyName is null) diff --git a/src/Acornima/Tokenizer.cs b/src/Acornima/Tokenizer.cs index 149a1855..232d508e 100644 --- a/src/Acornima/Tokenizer.cs +++ b/src/Acornima/Tokenizer.cs @@ -134,6 +134,7 @@ private void ReadToken(TokenContext currentContext) } } + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] internal bool TryReadToken(int cp) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/tokenize.js > `pp.readToken = function`, `pp.getTokenFromCode = function` @@ -318,6 +319,7 @@ private void SkipLineComment(int startSkip, CommentKind kind, OnCommentHandler? // Called at the start of the parse and after every token. Skips // whitespace and comments. + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private void SkipSpace(OnCommentHandler? onComment) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/tokenize.js > `pp.skipSpace = function` @@ -1519,6 +1521,7 @@ private int ReadHexChar(int len, int start, bool isVariableLength = false) // // Incrementally adds only escaped chars, adding other chunks as-is // as a micro-optimization. + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private ReadOnlySpan ReadWord1() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/tokenize.js > `pp.readWord1 = function` @@ -1578,6 +1581,7 @@ private ReadOnlySpan ReadWord1() // Read an identifier or keyword token. Will check for reserved // words when necessary. + [MethodImpl((MethodImplOptions)512 /* AggressiveOptimization */)] private bool ReadWord() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/tokenize.js > `pp.readWord = function` From ef78a2c48a1c14786a9977d1d0cf61c871bc4ea9 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Sun, 8 Mar 2026 22:35:04 +0100 Subject: [PATCH 2/3] Further improve ParseClassElement --- src/Acornima/Parser.Statement.cs | 81 +++++++++++++++----------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/Acornima/Parser.Statement.cs b/src/Acornima/Parser.Statement.cs index f0e931a8..889bc33c 100644 --- a/src/Acornima/Parser.Statement.cs +++ b/src/Acornima/Parser.Statement.cs @@ -1356,14 +1356,12 @@ private Node ParseClassElement(bool constructorAllowsSuper) var hasDecorators = _tokenizer._type == TokenType.At; var decorators = hasDecorators ? ParseDecorators() : default; + bool isStatic, isAsync, isGenerator, isAccessor = false; string? keyName = null; string keyword; - bool isAsync = false; - bool isGenerator = false; - bool isAccessor = false; var kind = PropertyKind.Unknown; - var isStatic = EatContextual(keyword = "static"); + isStatic = EatContextual(keyword = "static"); if (isStatic) { // Parse static init block @@ -1379,7 +1377,7 @@ private Node ParseClassElement(bool constructorAllowsSuper) if (!(IsClassElementNameStart() || _tokenizer._type == TokenType.Star)) { - isStatic = false; + isStatic = isAsync = isGenerator = false; keyName = keyword; goto ParseElementName; } @@ -1390,60 +1388,59 @@ private Node ParseClassElement(bool constructorAllowsSuper) { if (!((IsClassElementNameStart() || _tokenizer._type == TokenType.Star) && !CanInsertSemicolon())) { - isAsync = false; + isAsync = isGenerator = false; keyName = keyword; goto ParseElementName; } - } - isGenerator = // NOTE: We only need to check the ECMA version in the case of async generators // since non-async generators were introduced in ES6, together with classes. - (!isAsync || _tokenizerOptions._ecmaVersion >= EcmaVersion.ES9) - && Eat(TokenType.Star); + isGenerator = _tokenizerOptions._ecmaVersion >= EcmaVersion.ES9 && Eat(TokenType.Star); + goto ParseElementName; + } + + isGenerator = Eat(TokenType.Star); + if (isGenerator) + { + goto ParseElementName; + } - if (!isAsync && !isGenerator) + if (EatContextual(keyword = "get")) { - if (EatContextual(keyword = "get")) + if (IsClassElementNameStart()) { - if (IsClassElementNameStart()) - { - kind = PropertyKind.Get; - } - else - { - keyName = keyword; - goto ParseElementName; - } + kind = PropertyKind.Get; } - else if (EatContextual(keyword = "set")) + else { - if (IsClassElementNameStart()) - { - kind = PropertyKind.Set; - } - else - { - keyName = keyword; - goto ParseElementName; - } + keyName = keyword; + } + } + else if (EatContextual(keyword = "set")) + { + if (IsClassElementNameStart()) + { + kind = PropertyKind.Set; } - else if (_tokenizerOptions.AllowDecorators() && EatContextual(keyword = "accessor")) + else { - if (!CanInsertSemicolon() && IsClassElementNameStart()) - { - isAccessor = true; - } - else - { - keyName = keyword; - goto ParseElementName; - } + keyName = keyword; + } + } + else if (_tokenizerOptions.AllowDecorators() && EatContextual(keyword = "accessor")) + { + if (!CanInsertSemicolon() && IsClassElementNameStart()) + { + isAccessor = true; + } + else + { + keyName = keyword; } } // Parse element name - ParseElementName: + ParseElementName: Expression key; bool computed; if (keyName is null) From cc1afbbda7f7a8da076c0e7e70a5e4d5357db93d Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Sun, 8 Mar 2026 22:35:57 +0100 Subject: [PATCH 3/3] Update recursion depth for CanHandleDeepRecursion --- test/Acornima.Tests/ParserTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Acornima.Tests/ParserTests.cs b/test/Acornima.Tests/ParserTests.cs index 440cfee3..6465cff9 100644 --- a/test/Acornima.Tests/ParserTests.cs +++ b/test/Acornima.Tests/ParserTests.cs @@ -91,7 +91,7 @@ public void CanHandleDeepRecursion() #if DEBUG const int depth = 395; #else - const int depth = 610; + const int depth = 900; #endif var input = $"if ({new string('(', depth)}true{new string(')', depth)}) {{ }}"; parser.ParseScript(input);