daniellansun commented on PR #2921:
URL: https://github.com/apache/groovy/pull/2921#issuecomment-5639709184

   # Parser performance verification: GROOVY-12398
   
   **Question.** Does commit `4c89f6774d5cbb431940b0ca9d1581a9434b0d1e` parse a 
fixed Groovy corpus faster than commit 
`4aa7b94b83bd29814b1449cde1b239bdbe8c0ff0`, when both use the same parse-only 
path that production uses (SLL, then LL on failure)?
   
   **Answer.** Yes. On this host, with a frozen corpus of 245 files (570 533 
characters), the target parser used **about 35% less wall time** than the 
baseline (pooled mean **1.53×**). Every post-warmup sample of the target was 
faster than every post-warmup sample of the baseline. Parse outcomes (SLL 
success, LL fallback, the one expected failure) are identical.
   
   This is a **parse-only** measurement. It does not claim the same factor for 
end-to-end `groovyc`.
   
   ---
   
   ## 1. What was compared
   
   | | Baseline | Target |
   |---|---|---|
   | Commit | `4aa7b94b83bd29814b1449cde1b239bdbe8c0ff0` | 
`4c89f6774d5cbb431940b0ca9d1581a9434b0d1e` |
   | Subject | `prepare for version 7 (cont'd)` | `GROOVY-12398: classify 
supplementary-plane identifiers by code point` |
   | Raw jar SHA-256 | 
`83130ad2d6ce26e1fee7eccca7a9f387151142defb923d3af1087f324cb7f8c7` | 
`17946a113b5123939a6ecff762a3ffa62535bbb8d0444e39a928acf7a736cbe8` |
   
   The range contains two commits:
   
   1. `cfcc6d50fc` — lexer/parser grammar and predicate work (the performance 
change).
   2. `4c89f6774d` — review follow-up (code-point capitalization, invalid-octal 
caret, tests). That follow-up is not expected to move the hot path; the 
measurement is of the **full** GROOVY-12398 series against the parent of the 
first commit.
   
   Each commit was built in its own Git worktree (`./gradlew :jar`). The jars 
were checked before timing: baseline still contains `invalidDigitCount` and 
`java.util.regex.Pattern`; target contains `lastConsumedCodePoint` and 
`isIdentifierAssign`.
   
   ---
   
   ## 2. Method
   
   ### Corpus
   
   `src/main/groovy` + `src/test-resources/core`, `*.groovy` only, walked in 
sorted path order.
   
   The tree was **frozen from the baseline commit** and reused for every run, 
so a change in source text cannot be mistaken for a parser change. The same two 
directories at the target commit hash to the same SHA-256 (39 + 206 files). 
Input is therefore identical in both trees.
   
   | Root | Files | UTF-16/UTF-8 chars | Bytes |
   |---|---:|---:|---:|
   | `src/main/groovy` | 39 | 290 264 | 290 270 |
   | `src/test-resources/core` | 206 | 280 269 | 280 277 |
   | **Total** | **245** | **570 533** | **570 547** |
   
   ### Parse path
   
   Matches `AstBuilder`:
   
   1. `GroovyLangLexer` + `CommonTokenStream` + `GroovyLangParser`
   2. `BailErrorStrategy`, no console listeners
   3. `PredictionMode.SLL` first
   4. On failure: `tokens.seek(0)`, `parser.reset()`, `PredictionMode.LL`
   5. New lexer/parser per file; ATN/DFA shared through `ParserAtnManager` 
(production-like cache warmup)
   
   ANTLR runtime: `me.sunlan:antlr4-runtime:4.13.2.16` (the runtime Groovy 
already uses).
   
   ### Timing protocol
   
   - Independent JVM per invocation (`-Xms1g -Xmx1g -XX:+AlwaysPreTouch`).
   - 5 untimed warmup passes (DFA fill + JIT), then **15 timed** full-corpus 
passes.
   - Wall time is `System.nanoTime()` around the whole corpus, not per file.
   - Invocation order was interleaved **A B A B A B A** (baseline / target / 
baseline / …) so a slow host cannot be blamed on “target ran first while the 
machine was idle”.
   - 4 baseline JVMs × 15 = **60** timed samples; 3 target JVMs × 15 = **45** 
timed samples.
   
   ### Host
   
   | | |
   |---|---|
   | Kernel | Linux 6.15.5 x86_64 (`hera`) |
   | CPU | AMD EPYC 7763, 6 visible CPUs, 1 thread/core |
   | Memory | 24 GiB |
   | JVM | Corretto 25.0.4+7-LTS, mixed mode |
   | Date | 2026-09-11 (UTC) |
   
   This is a shared developer VM. Variance across baseline JVMs is reported 
rather than hidden.
   
   ---
   
   ## 3. Correctness (same corpus, both parsers)
   
   | Outcome | Baseline | Target |
   |---|---:|---:|
   | SLL success | 241 | 241 |
   | LL fallback | 3 | 3 |
   | Hard failure | 1 | 1 |
   
   LL fallback (both parsers):
   
   - `src/test-resources/core/Command_01.groovy`
   - `src/test-resources/core/Command_05.groovy`
   - `src/test-resources/core/Lambda_01x.groovy`
   
   Hard failure (both parsers): `src/test-resources/core/Unicode_01.groovy` — 
`GroovySyntaxError: Illegal escape character: '\u'`. That file is an 
illegal-escape fixture, not a regression.
   
   The speedup is **not** from fewer LL retries. The SLL/LL split is unchanged.
   
   ---
   
   ## 4. Results
   
   Times are seconds to parse the whole corpus after warmup. Lower is better.
   
   ### Per JVM (15 timed rounds each)
   
   | Run | Role | Min | Median | Mean | Stdev | Max |
   |---|---|---:|---:|---:|---:|---:|
   | A | baseline | 0.218082 | 0.233826 | 0.242895 | 0.022486 | 0.293010 |
   | B | target | 0.148911 | 0.177118 | 0.177364 | 0.019880 | 0.211886 |
   | A2 | baseline | 0.254599 | 0.295294 | 0.300075 | 0.036579 | 0.381228 |
   | B2 | target | 0.147442 | 0.165518 | 0.166205 | 0.009369 | 0.185643 |
   | A3 | baseline | 0.222311 | 0.236567 | 0.242742 | 0.018481 | 0.291574 |
   | B3 | target | 0.147241 | 0.162760 | 0.167201 | 0.016246 | 0.204381 |
   | A4 | baseline | 0.219386 | 0.260223 | 0.255703 | 0.027844 | 0.326735 |
   
   A2 is a noisy baseline JVM (mean 0.300 s vs ~0.243 s for A and A3). It is 
kept in the pool rather than discarded.
   
   ### Pooled post-warmup samples
   
   | | n | Min | Median | Mean | Stdev | Max |
   |---|---:|---:|---:|---:|---:|---:|
   | Baseline | 60 | 0.218082 | 0.252503 | **0.260354** | 0.035573 | 0.381228 |
   | Target | 45 | 0.147241 | 0.165395 | **0.170257** | 0.016237 | 0.211886 |
   
   ### Speedup
   
   | Estimator | Baseline | Target | Factor | Time saved |
   |---|---:|---:|---:|---:|
   | Mean | 0.260354 s | 0.170257 s | **1.53×** | 34.6% |
   | Median | 0.252503 s | 0.165395 s | **1.53×** | 34.5% |
   | Best (min) | 0.218082 s | 0.147241 s | **1.48×** | 32.5% |
   
   Throughput at the pooled means: **941 → 1 439 files/s**, **2.19e6 → 3.35e6 
chars/s**.
   
   ### Separation
   
   The slowest target round (0.211886 s) is still faster than the fastest 
baseline round (0.218082 s). The two sample sets do not overlap.
   
   Cohen’s *d* on the pooled samples is **3.1** (large). That is a description 
of this data set, not a claim about every machine.
   
   ### Conservative cross-check
   
   Against the **fastest** baseline JVM mean (A3, 0.242742 s) and the 
**slowest** target JVM mean (B, 0.177364 s), the factor is still **1.37×**. 
That is the least flattering pairing of JVM means in this run.
   
   ---
   
   ## 5. What this does and does not show
   
   **Shown**
   
   - On this host, the GROOVY-12398 parser is consistently faster on this 
corpus than its parent, by roughly one and a half times in parse-only wall time.
   - The improvement is in the SLL-successful majority (241/245 files), not in 
a change of prediction mode mix.
   - The corpus and the parse outcomes are the same, so the delta is in the 
recognisers, not in “different files” or “more failures skipped”.
   
   **Not shown**
   
   - End-to-end compile time (semantic analysis, classgen, I/O). Parse is only 
part of `groovyc`; a 1.5× parse-only gain will shrink to a smaller fraction of 
a full compile, as already noted on the PR.
   - Other hardware, JDK 17/21, or a Grails/Spock-sized corpus. Those would be 
the right next measurement if the change is considered for 6.0.
   - That every future Groovy file will see 1.5×. Command-heavy scripts that 
already fall back to LL (three files here) are not the bulk of this corpus.
   
   **Noise**
   
   Baseline JVM A2 was ~24% slower than A/A3. The machine is not a quiet 
benchmark box. Interleaving and pooling are how that is handled; dropping A2 
would only make the target look better, so it was not dropped.
   
   ---
   
   ## 6. Conclusion
   
   Relative to `4aa7b94b83`, commit `4c89f6774d` (GROOVY-12398 as it stands on 
that branch) delivers a **real, repeatable parse-only speedup** of about 
**1.5×** (≈35% less time) on `src/main/groovy` + `src/test-resources/core`. 
Parse behaviour on that corpus is unchanged.
   
   That is sufficient evidence for a **master** merge from a performance 
standpoint. It is **not** by itself a case for slipping the same grammar/ATN 
rewrite into a 6.0 RC without a further compile-level corpus check.
   
   ---
   
   ## Appendix: how to reproduce
   
   ```bash
   # worktrees
   git worktree add --detach /tmp/parse-verify/wt-baseline 
4aa7b94b83bd29814b1449cde1b239bdbe8c0ff0
   git worktree add --detach /tmp/parse-verify/wt-target   
4c89f6774d5cbb431940b0ca9d1581a9434b0d1e
   ( cd /tmp/parse-verify/wt-baseline && ./gradlew :jar --offline -q )
   ( cd /tmp/parse-verify/wt-target   && ./gradlew :jar --offline -q )
   
   # freeze corpus from baseline
   mkdir -p /tmp/parse-verify/corpus
   cp -a /tmp/parse-verify/wt-baseline/src/main/groovy          
/tmp/parse-verify/corpus/main-groovy
   cp -a /tmp/parse-verify/wt-baseline/src/test-resources/core  
/tmp/parse-verify/corpus/test-resources-core
   
   
ANTLR=$HOME/.gradle/caches/modules-2/files-2.1/me.sunlan/antlr4-runtime/4.13.2.16/9c7caa836c70ef09ee7b70f77c11b803f75e19c0/antlr4-runtime-4.13.2.16.jar
   # ParseBench.java as used for this report (5 warmup, 15 rounds, SLL then LL)
   java -Xms1g -Xmx1g -XX:+AlwaysPreTouch -Dbench.label=... \
     -cp ".:<raw-jar>:$ANTLR" ParseBench \
     /tmp/parse-verify/corpus/main-groovy \
     /tmp/parse-verify/corpus/test-resources-core
   ```
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to