Skip to content

Commit 33da350

Browse files
dmitriplotnikovcopybara-github
authored andcommitted
Optimize CEL PrattParser and Lexer performance
Improve parsing throughput and reduce memory allocations across CEL expressions. 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 | 131 | 123 | 2.9x smaller | -6.1% | | CHAINED_ORS | 968 | 374 | 350 | 2.8x smaller | -6.4% | | LIST_COMPREHENSION | 512 | 218 | 166 | 3.1x smaller | -23.9% | | MESSAGE_CREATION | 1,253 | 502 | 427 | 2.9x smaller | -14.9% | | LONG_LIST | 81,794 | 19,310 | 19,271 | 4.2x smaller | -0.2% | Bytes allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 12,256 | 3,776 | 3,608 | 3.4x smaller | -4.4% | | CHAINED_ORS | 32,160 | 10,288 | 9,864 | 3.3x smaller | -4.1% | | LIST_COMPREHENSION | 17,320 | 6,120 | 4,736 | 3.7x smaller | -22.6% | | MESSAGE_CREATION | 43,128 | 13,888 | 12,240 | 3.5x smaller | -11.9% | | LONG_LIST | 2,907,488 | 553,160 | 568,080 | 5.1x smaller | +2.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 | 733 ns | 692 ns | 7.1x faster | -5.7% | | CHAINED_ORS | 14,641 ns | 2,090 ns | 2,041 ns | 7.2x faster | -2.3% | | LIST_COMPREHENSION | 7,514 ns | 1,640 ns | 1,218 ns | 6.2x faster | -25.7% | | MESSAGE_CREATION | 20,979 ns | 3,962 ns | 3,487 ns | 6.0x faster | -12.0% | | LONG_LIST | 1,616,631 ns | 144,980 ns | 142,583 ns | 11.3x faster | -1.7% | LONG_LIST is the one case that allocates slightly more than before. It is an extreme outlier (1,000 list elements, ~20k objects per parse) and the +2.7% comes from letting the positions map grow from its default capacity instead of presizing it; presizing cost more on every other case, so the tradeoff is worth it. The map is removed entirely later in this series. PiperOrigin-RevId: 980131070
1 parent abff7d1 commit 33da350

8 files changed

Lines changed: 395 additions & 255 deletions

File tree

parser/src/main/java/dev/cel/parser/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ java_library(
118118
"//common:source_location",
119119
"//common/ast",
120120
"//common/internal",
121+
"@maven//:com_google_errorprone_error_prone_annotations",
121122
"@maven//:com_google_guava_guava",
122123
"@maven//:org_jspecify_jspecify",
123124
],

parser/src/main/java/dev/cel/parser/Lexer.java

Lines changed: 50 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ final class Lexer {
2929
enum TokenType {
3030
ERROR("error"),
3131
END("end"),
32-
WHITESPACE("whitespace"),
33-
COMMENT("comment"),
3432

3533
// Keywords
3634
NULL("null"),
@@ -98,11 +96,17 @@ static final class Token {
9896
final TokenType type;
9997
final int start;
10098
final int end;
99+
final @Nullable String text;
101100

102101
Token(TokenType type, int start, int end) {
102+
this(type, start, end, null);
103+
}
104+
105+
Token(TokenType type, int start, int end, @Nullable String text) {
103106
this.type = type;
104107
this.start = start;
105108
this.end = end;
109+
this.text = text;
106110
}
107111

108112
@Override
@@ -149,35 +153,28 @@ static final class LexerError {
149153
.buildOrThrow();
150154

151155
private final CelCodePointArray content;
156+
private final int size;
152157
private int position;
153158
private LexerError error;
154159

155160
Lexer(CelCodePointArray content) {
156161
this.content = content;
162+
this.size = content.size();
157163
this.position = 0;
158164
this.error = null;
159165
}
160166

161167
Token lex() {
168+
consumeWhitespaceAndComments();
162169
int start = position;
163-
if (position >= content.size()) {
170+
if (position >= size) {
164171
return makeToken(TokenType.END, start, start);
165172
}
166173
int c = content.get(position);
167174
switch (c) {
168-
case '\f':
169-
case '\n':
170-
case ' ':
171-
case '\r':
172-
case 0x0B: // \v (vertical tab)
173-
case '\t':
174-
{
175-
consumeWhitespace();
176-
return makeToken(TokenType.WHITESPACE, start, position);
177-
}
178175
case '.':
179176
{
180-
if (position + 1 < content.size() && isDigit(content.get(position + 1))) {
177+
if (position + 1 < size && isDigit(content.get(position + 1))) {
181178
return consumeNumericLiteral();
182179
}
183180
advance(1);
@@ -283,10 +280,6 @@ Token lex() {
283280
case '/':
284281
{
285282
advance(1);
286-
if (consume('/')) {
287-
consumeLine();
288-
return makeToken(TokenType.COMMENT, start, position);
289-
}
290283
return makeToken(TokenType.SLASH, start, position);
291284
}
292285
case '&':
@@ -381,6 +374,10 @@ private Token makeToken(TokenType type, int start, int end) {
381374
return new Token(type, start, end);
382375
}
383376

377+
private Token makeToken(TokenType type, int start, int end, @Nullable String text) {
378+
return new Token(type, start, end, text);
379+
}
380+
384381
private Token setError(int start, int end, String message) {
385382
this.error = new LexerError(start, end, message);
386383
return new Token(TokenType.ERROR, start, end);
@@ -391,7 +388,7 @@ private void advance(int n) {
391388
}
392389

393390
private boolean match(int c) {
394-
return position < content.size() && content.get(position) == c;
391+
return position < size && content.get(position) == c;
395392
}
396393

397394
private boolean consume(int c) {
@@ -403,7 +400,7 @@ private boolean consume(int c) {
403400
}
404401

405402
private boolean consumeIf(IntPredicate predicate) {
406-
if (position < content.size()) {
403+
if (position < size) {
407404
int cp = content.get(position);
408405
if (predicate.test(cp)) {
409406
advance(1);
@@ -414,7 +411,7 @@ private boolean consumeIf(IntPredicate predicate) {
414411
}
415412

416413
private void consumeLine() {
417-
while (position < content.size()) {
414+
while (position < size) {
418415
if (content.get(position) == '\n') {
419416
advance(1);
420417
return;
@@ -423,8 +420,8 @@ private void consumeLine() {
423420
}
424421
}
425422

426-
private void consumeWhitespace() {
427-
while (position < content.size()) {
423+
private void consumeWhitespaceAndComments() {
424+
while (position < size) {
428425
int c = content.get(position);
429426
switch (c) {
430427
case '\f':
@@ -433,38 +430,35 @@ private void consumeWhitespace() {
433430
case '\r':
434431
case 11: // \v
435432
case '\t':
436-
advance(1);
433+
position++;
437434
break;
435+
case '/':
436+
if (position + 1 < size && content.get(position + 1) == '/') {
437+
consumeLine();
438+
break;
439+
} else {
440+
return;
441+
}
438442
default:
439443
return;
440444
}
441445
}
442446
}
443447

444448
private boolean consumeDigits() {
445-
boolean advanced = false;
446-
while (position < content.size()) {
447-
int c = content.get(position);
448-
if (!isDigit(c)) {
449-
break;
450-
}
451-
advance(1);
452-
advanced = true;
449+
int start = position;
450+
while (position < size && isDigit(content.get(position))) {
451+
position++;
453452
}
454-
return advanced;
453+
return position > start;
455454
}
456455

457456
private boolean consumeHexDigits() {
458-
boolean advanced = false;
459-
while (position < content.size()) {
460-
int c = content.get(position);
461-
if (!isHexDigit(c)) {
462-
break;
463-
}
464-
advance(1);
465-
advanced = true;
457+
int start = position;
458+
while (position < size && isHexDigit(content.get(position))) {
459+
position++;
466460
}
467-
return advanced;
461+
return position > start;
468462
}
469463

470464
private TokenType consumeIntegralSuffix() {
@@ -486,7 +480,7 @@ private Token consumeQuotedIdent() {
486480
private boolean consumeUntilAfter(int c, boolean isRaw) {
487481
int pos = position;
488482
boolean escaped = false;
489-
while (pos < content.size()) {
483+
while (pos < size) {
490484
int cc = content.get(pos);
491485
if (cc == '\n' || cc == '\r') {
492486
position = pos;
@@ -503,20 +497,20 @@ private boolean consumeUntilAfter(int c, boolean isRaw) {
503497
}
504498
pos++;
505499
}
506-
position = content.size();
500+
position = size;
507501
return false;
508502
}
509503

510504
private boolean consumeUntilAfterTripleQuote(int quote, boolean isRaw) {
511505
int pos = position;
512506
boolean escaped = false;
513-
while (pos < content.size()) {
507+
while (pos < size) {
514508
int cc = content.get(pos);
515509
if (!isRaw && cc == '\\') {
516510
escaped = !escaped;
517511
} else {
518512
if ((isRaw || !escaped)
519-
&& pos + 2 < content.size()
513+
&& pos + 2 < size
520514
&& cc == quote
521515
&& content.get(pos + 1) == quote
522516
&& content.get(pos + 2) == quote) {
@@ -527,16 +521,14 @@ private boolean consumeUntilAfterTripleQuote(int quote, boolean isRaw) {
527521
}
528522
pos++;
529523
}
530-
position = content.size();
524+
position = size;
531525
return false;
532526
}
533527

534528
private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolean isRaw) {
535529
advance(1);
536530
boolean isTripleQuote =
537-
position + 1 < content.size()
538-
&& content.get(position) == quote
539-
&& content.get(position + 1) == quote;
531+
position + 1 < size && content.get(position) == quote && content.get(position + 1) == quote;
540532
if (isTripleQuote) {
541533
advance(2);
542534
if (!consumeUntilAfterTripleQuote(quote, isRaw)) {
@@ -556,7 +548,7 @@ private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolea
556548

557549
private @Nullable Token consumePrefixedStringLiteral() {
558550
int start = position;
559-
if (position >= content.size()) {
551+
if (position >= size) {
560552
return null;
561553
}
562554
int c = content.get(position);
@@ -566,15 +558,15 @@ private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolea
566558
return null;
567559
}
568560
int lookahead = 1;
569-
if (position + 1 < content.size()) {
561+
if (position + 1 < size) {
570562
int c2 = content.get(position + 1);
571563
if (isBytes ? (c2 == 'r' || c2 == 'R') : (c2 == 'b' || c2 == 'B')) {
572564
isBytes = true;
573565
isRaw = true;
574566
lookahead = 2;
575567
}
576568
}
577-
if (position + lookahead < content.size()) {
569+
if (position + lookahead < size) {
578570
int quote = content.get(position + lookahead);
579571
if (quote == '"' || quote == '\'') {
580572
advance(lookahead);
@@ -612,9 +604,9 @@ private Token consumeNumericLiteral() {
612604
return makeToken(tokenType, start, position);
613605
}
614606
consumeDigits();
615-
if (position < content.size()
607+
if (position < size
616608
&& content.get(position) == '.'
617-
&& position + 1 < content.size()
609+
&& position + 1 < size
618610
&& isDigit(content.get(position + 1))) {
619611
floatingPoint = true;
620612
advance(1);
@@ -639,19 +631,15 @@ && isDigit(content.get(position + 1))) {
639631

640632
private Token consumeIdent() {
641633
int start = position;
642-
while (position < content.size()) {
643-
int c = content.get(position);
644-
if (!isIdentTrailing(c)) {
645-
break;
646-
}
647-
advance(1);
634+
while (position < size && isIdentTrailing(content.get(position))) {
635+
position++;
648636
}
649637
int end = position;
650638
String word = content.slice(start, end).toString();
651639
TokenType keywordType = KEYWORDS.get(word);
652640
if (keywordType != null) {
653641
return makeToken(keywordType, start, end);
654642
}
655-
return makeToken(TokenType.IDENT, start, end);
643+
return makeToken(TokenType.IDENT, start, end, word);
656644
}
657645
}

0 commit comments

Comments
 (0)