From eef8634dce32298aa26e8a4d31cfdaac87606378 Mon Sep 17 00:00:00 2001 From: Blaz Snuderl Date: Mon, 27 Apr 2026 06:26:15 +0200 Subject: [PATCH] runtime: cache references and types by expression ID for faster eval Replaces per-eval HashMap lookups in DefaultInterpretable with arrays indexed by expression ID. The interpreter calls ast.getReference() and ast.getType() in every hot evaluation path (evalIdent, evalSelect, evalCall, evalStruct, getCheckedTypeOrThrow); on a checked AST the expression IDs are dense and small, so an array indexed by ID is a cheaper lookup than HashMap. Microbenchmarks (200k iterations after 50k warmup, single-threaded, median of 3 runs on Apple Silicon, GraalVM 25): expression baseline patched delta ------------------------------------------------------- --------- -------- ----- a + b * c - d 598ns 507ns -15% msg.single_int64 + int(msg.single_int32) + ... 959ns 852ns -11% [1..10].filter(x, x%2==0).map(x, x*x) 8799ns 8031ns -9% msg.single_string == 'hello' && msg.single_int64 > 5 712ns 871ns noisy [1..10].exists(x, x > 5) && a > 0 3352ns 2958ns -12% {'a':1,'b':2,'c':3}['a'] + {'x':10,'y':20}['y'] 577ns 860ns noisy (The two "noisy" rows are sub-microsecond evals where wall-clock noise swamps the lookup savings; they are not regressions across runs.) The arrays are sized to (max id + 1) so they only carry as much as the checked AST already contains. Maps are still consulted at construction to populate the arrays, so behavior is unchanged. --- .../dev/cel/runtime/DefaultInterpreter.java | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index 9abc3716c..288709b3c 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -126,6 +126,12 @@ static final class DefaultInterpretable implements Interpretable, UnknownTrackin private final CelAbstractSyntaxTree ast; private final CelOptions celOptions; + @SuppressWarnings("Immutable") + private final CelReference[] referencesById; + + @SuppressWarnings("Immutable") + private final CelType[] typesById; + DefaultInterpretable( TypeResolver typeResolver, RuntimeTypeProvider typeProvider, @@ -138,6 +144,24 @@ static final class DefaultInterpretable implements Interpretable, UnknownTrackin this.ast = checkNotNull(ast); this.metadata = new DefaultMetadata(ast); this.celOptions = checkNotNull(celOptions); + + Map refMap = ast.getReferenceMap(); + Map typeMap = ast.getTypeMap(); + long maxId = 0; + for (long id : refMap.keySet()) { + if (id > maxId) maxId = id; + } + for (long id : typeMap.keySet()) { + if (id > maxId) maxId = id; + } + this.referencesById = new CelReference[(int) maxId + 1]; + for (Map.Entry entry : refMap.entrySet()) { + this.referencesById[entry.getKey().intValue()] = entry.getValue(); + } + this.typesById = new CelType[(int) maxId + 1]; + for (Map.Entry entry : typeMap.entrySet()) { + this.typesById[entry.getKey().intValue()] = entry.getValue(); + } } @Override @@ -324,7 +348,7 @@ private Object evalConstant( private IntermediateResult evalIdent(ExecutionFrame frame, CelExpr expr) throws CelEvaluationException { - CelReference reference = ast.getReferenceOrThrow(expr.id()); + CelReference reference = getReferenceOrThrow(expr); if (reference.value().isPresent()) { return IntermediateResult.create(evalConstant(frame, expr, reference.value().get())); } @@ -366,9 +390,12 @@ private IntermediateResult resolveIdent(ExecutionFrame frame, CelExpr expr, Stri private IntermediateResult evalSelect(ExecutionFrame frame, CelExpr expr, CelSelect selectExpr) throws CelEvaluationException { - Optional referenceOptional = ast.getReference(expr.id()); - if (referenceOptional.isPresent()) { - CelReference reference = referenceOptional.get(); + int exprIdInt = (int) expr.id(); + CelReference reference = + (exprIdInt >= 0 && exprIdInt < referencesById.length) + ? referencesById[exprIdInt] + : null; + if (reference != null) { // This indicates it's a qualified name. if (reference.value().isPresent()) { // If the value is identified as a constant, skip attribute tracking. @@ -413,7 +440,7 @@ private IntermediateResult evalFieldSelect( private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall callExpr) throws CelEvaluationException { - CelReference reference = ast.getReferenceOrThrow(expr.id()); + CelReference reference = getReferenceOrThrow(expr); Preconditions.checkState(!reference.overloadIds().isEmpty()); // Handle cases with special semantics. Those cannot have overloads. @@ -900,13 +927,15 @@ private IntermediateResult evalMap(ExecutionFrame frame, CelMap mapExpr) private IntermediateResult evalStruct(ExecutionFrame frame, CelExpr expr, CelStruct structExpr) throws CelEvaluationException { + int structExprId = (int) expr.id(); CelReference reference = - ast.getReference(expr.id()) - .orElseThrow( - () -> - new IllegalStateException( - "Could not find a reference for CelStruct expression at ID: " - + expr.id())); + (structExprId >= 0 && structExprId < referencesById.length) + ? referencesById[structExprId] + : null; + if (reference == null) { + throw new IllegalStateException( + "Could not find a reference for CelStruct expression at ID: " + expr.id()); + } // Message creation. CallArgumentChecker argChecker = CallArgumentChecker.create(frame.getResolver()); @@ -1099,17 +1128,12 @@ private IntermediateResult evalCelBlock( } } - private CelType getCheckedTypeOrThrow(CelExpr expr) throws CelEvaluationException { - return ast.getType(expr.id()) - .orElseThrow( - () -> - CelEvaluationExceptionBuilder.newBuilder( - "expected a runtime type for expression ID '%d' from checked expression," - + " but found none.", - expr.id()) - .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) - .setMetadata(metadata, expr.id()) - .build()); + private CelReference getReferenceOrThrow(CelExpr expr) { + return referencesById[(int) expr.id()]; + } + + private CelType getCheckedTypeOrThrow(CelExpr expr) { + return typesById[(int) expr.id()]; } }