Skip to content

Avoid intermediate slice views when reading token text - #1218

Open
copybara-service[bot] wants to merge 1 commit into
mainfrom
test_979610652
Open

Avoid intermediate slice views when reading token text#1218
copybara-service[bot] wants to merge 1 commit into
mainfrom
test_979610652

Conversation

@copybara-service

Copy link
Copy Markdown
Contributor

Avoid intermediate slice views when reading token text

Lexing and parsing turn code point ranges into strings constantly: once for
every identifier, keyword and literal. Both call sites spelled this as
slice(i, j).toString(), which allocates an intermediate CelCodePointArray view
solely to copy out of it and then discard it.

Add CelCodePointArray.substring(i, j), which builds the String straight from the
backing array, and implement it in each of the four subclasses. toString()
becomes final and delegates to substring(0, size()), so the subclasses lose
their near-duplicate toString() overrides. Lexer.consumeIdent and
PrattParser.getTokenText call the new method.

This removes exactly one 32-byte object per token whose text is materialized.

Measured with CelParserBenchmark (parseOnly, built -c opt), comparing three
parsers back to back in one session: ANTLR, the Pratt parser before this change,
and the Pratt parser after it.

Objects allocated per parse:

Case ANTLR Pratt before Pratt after Pratt vs ANTLR Delta this CL
SMOKE_TEST 357 123 120 3.0x smaller -2.4%
CHAINED_ORS 968 349 339 2.9x smaller -2.9%
LIST_COMPREHENSION 512 166 160 3.2x smaller -3.6%
MESSAGE_CREATION 1,253 426 406 3.1x smaller -4.7%
LONG_LIST 81,794 19,265 18,263 4.5x smaller -5.2%

Bytes allocated per parse:

Case ANTLR Pratt before Pratt after Pratt vs ANTLR Delta this CL
SMOKE_TEST 12,256 3,608 3,512 3.5x smaller -2.7%
CHAINED_ORS 32,160 9,912 9,592 3.4x smaller -3.2%
LIST_COMPREHENSION 17,320 4,928 4,736 3.7x smaller -3.9%
MESSAGE_CREATION 43,128 13,056 12,416 3.5x smaller -4.9%
LONG_LIST 2,907,488 563,952 531,888 5.5x smaller -5.7%

Wall clock, mean of 3 caliper trial medians:

Case ANTLR Pratt before Pratt after Pratt vs ANTLR Delta this CL
SMOKE_TEST 4,940 ns 696 ns 684 ns 7.2x faster -1.8%
CHAINED_ORS 14,641 ns 2,008 ns 2,006 ns 7.3x faster -0.1%
LIST_COMPREHENSION 7,514 ns 1,250 ns 1,217 ns 6.2x faster -2.7%
MESSAGE_CREATION 20,979 ns 3,526 ns 3,514 ns 6.0x faster -0.4%
LONG_LIST 1,616,631 ns 140,500 ns 146,640 ns 11.0x faster +4.4%

Wall clock is unchanged within measurement noise. The per-case deltas run from
-2.7% to +4.4% and straddle zero, which is what a change that removes 3-5% of
allocations and no actual work should look like. The LONG_LIST row reads as a
regression, but that trial was noisy (per-trial medians 144.6us, 155.7us,
139.6us, against a much tighter 142.1us, 141.1us, 138.3us before) and its
fastest observed parse, 134.6us, is below the 135.7us baseline. The win here is
allocation volume and the GC pressure that follows from it. This is the first in
a series of parser changes; the wall-clock improvements come later in that
series.

The ANTLR column is included for scale, and shows why the Pratt parser exists.
ANTLR is slow enough on LONG_LIST that the case exceeds caliper's default 5
minute per-trial budget and has to be measured with a raised --time-limit.

Lexing and parsing turn code point ranges into strings constantly: once for
every identifier, keyword and literal. Both call sites spelled this as
slice(i, j).toString(), which allocates an intermediate CelCodePointArray view
solely to copy out of it and then discard it.

Add CelCodePointArray.substring(i, j), which builds the String straight from the
backing array, and implement it in each of the four subclasses. toString()
becomes final and delegates to substring(0, size()), so the subclasses lose
their near-duplicate toString() overrides. Lexer.consumeIdent and
PrattParser.getTokenText call the new method.

This removes exactly one 32-byte object per token whose text is materialized.

Measured with CelParserBenchmark (parseOnly, built -c opt), comparing three
parsers back to back in one session: ANTLR, the Pratt parser before this change,
and the Pratt parser after it.

Objects allocated per parse:

| Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL |
| :--- | ---: | ---: | ---: | ---: | ---: |
| SMOKE_TEST | 357 | 123 | 120 | 3.0x smaller | -2.4% |
| CHAINED_ORS | 968 | 349 | 339 | 2.9x smaller | -2.9% |
| LIST_COMPREHENSION | 512 | 166 | 160 | 3.2x smaller | -3.6% |
| MESSAGE_CREATION | 1,253 | 426 | 406 | 3.1x smaller | -4.7% |
| LONG_LIST | 81,794 | 19,265 | 18,263 | 4.5x smaller | -5.2% |

Bytes allocated per parse:

| Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL |
| :--- | ---: | ---: | ---: | ---: | ---: |
| SMOKE_TEST | 12,256 | 3,608 | 3,512 | 3.5x smaller | -2.7% |
| CHAINED_ORS | 32,160 | 9,912 | 9,592 | 3.4x smaller | -3.2% |
| LIST_COMPREHENSION | 17,320 | 4,928 | 4,736 | 3.7x smaller | -3.9% |
| MESSAGE_CREATION | 43,128 | 13,056 | 12,416 | 3.5x smaller | -4.9% |
| LONG_LIST | 2,907,488 | 563,952 | 531,888 | 5.5x smaller | -5.7% |

Wall clock, mean of 3 caliper trial medians:

| Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL |
| :--- | ---: | ---: | ---: | ---: | ---: |
| SMOKE_TEST | 4,940 ns | 696 ns | 684 ns | 7.2x faster | -1.8% |
| CHAINED_ORS | 14,641 ns | 2,008 ns | 2,006 ns | 7.3x faster | -0.1% |
| LIST_COMPREHENSION | 7,514 ns | 1,250 ns | 1,217 ns | 6.2x faster | -2.7% |
| MESSAGE_CREATION | 20,979 ns | 3,526 ns | 3,514 ns | 6.0x faster | -0.4% |
| LONG_LIST | 1,616,631 ns | 140,500 ns | 146,640 ns | 11.0x faster | +4.4% |

Wall clock is unchanged within measurement noise. The per-case deltas run from
-2.7% to +4.4% and straddle zero, which is what a change that removes 3-5% of
allocations and no actual work should look like. The LONG_LIST row reads as a
regression, but that trial was noisy (per-trial medians 144.6us, 155.7us,
139.6us, against a much tighter 142.1us, 141.1us, 138.3us before) and its
fastest observed parse, 134.6us, is below the 135.7us baseline. The win here is
allocation volume and the GC pressure that follows from it. This is the first in
a series of parser changes; the wall-clock improvements come later in that
series.

The ANTLR column is included for scale, and shows why the Pratt parser exists.
ANTLR is slow enough on LONG_LIST that the case exceeds caliper's default 5
minute per-trial budget and has to be measured with a raised --time-limit.

PiperOrigin-RevId: 979610652
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