Skip to content

Commit d522671

Browse files
l46kokcopybara-github
authored andcommitted
Optimize unary and binary function calls to avoid array allocation
PiperOrigin-RevId: 889403796
1 parent 576064d commit d522671

File tree

8 files changed

+242
-57
lines changed

8 files changed

+242
-57
lines changed

runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,20 @@ public interface CelFunctionBinding {
5454
@SuppressWarnings("unchecked")
5555
static <T> CelFunctionBinding from(
5656
String overloadId, Class<T> arg, CelFunctionOverload.Unary<T> impl) {
57-
return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]));
57+
return from(
58+
overloadId,
59+
ImmutableList.of(arg),
60+
new CelFunctionOverload() {
61+
@Override
62+
public Object apply(Object[] args) throws CelEvaluationException {
63+
return impl.apply((T) args[0]);
64+
}
65+
66+
@Override
67+
public Object apply(Object arg1) throws CelEvaluationException {
68+
return impl.apply((T) arg1);
69+
}
70+
});
5871
}
5972

6073
/**
@@ -65,7 +78,19 @@ static <T> CelFunctionBinding from(
6578
static <T1, T2> CelFunctionBinding from(
6679
String overloadId, Class<T1> arg1, Class<T2> arg2, CelFunctionOverload.Binary<T1, T2> impl) {
6780
return from(
68-
overloadId, ImmutableList.of(arg1, arg2), (args) -> impl.apply((T1) args[0], (T2) args[1]));
81+
overloadId,
82+
ImmutableList.of(arg1, arg2),
83+
new CelFunctionOverload() {
84+
@Override
85+
public Object apply(Object[] args) throws CelEvaluationException {
86+
return impl.apply((T1) args[0], (T2) args[1]);
87+
}
88+
89+
@Override
90+
public Object apply(Object arg1, Object arg2) throws CelEvaluationException {
91+
return impl.apply((T1) arg1, (T2) arg2);
92+
}
93+
});
6994
}
7095

7196
/** Create a function binding from the {@code overloadId}, {@code argTypes}, and {@code impl}. */

runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ public interface CelFunctionOverload {
2626
/** Evaluate a set of arguments throwing a {@code CelException} on error. */
2727
Object apply(Object[] args) throws CelEvaluationException;
2828

29+
/** Fast-path for unary function execution to avoid Object[] allocation. */
30+
default Object apply(Object arg) throws CelEvaluationException {
31+
return apply(new Object[] {arg});
32+
}
33+
34+
/** Fast-path for binary function execution to avoid Object[] allocation. */
35+
default Object apply(Object arg1, Object arg2) throws CelEvaluationException {
36+
return apply(new Object[] {arg1, arg2});
37+
}
38+
2939
/**
3040
* Helper interface for describing unary functions where the type-parameter is used to improve
3141
* compile-time correctness of function bindings.
@@ -57,27 +67,46 @@ static boolean canHandle(
5767
for (int i = 0; i < parameterTypes.size(); i++) {
5868
Class<?> paramType = parameterTypes.get(i);
5969
Object arg = arguments[i];
60-
if (arg == null) {
61-
// null can be assigned to messages, maps, and to objects.
62-
// TODO: Remove null special casing
63-
if (paramType != Object.class && !Map.class.isAssignableFrom(paramType)) {
64-
return false;
65-
}
66-
continue;
70+
boolean result = canHandleArg(arg, paramType, isStrict);
71+
if (!result) {
72+
return false;
6773
}
74+
}
75+
return true;
76+
}
6877

69-
if (arg instanceof Exception || arg instanceof CelUnknownSet) {
70-
// Only non-strict functions can accept errors/unknowns as arguments to a function
71-
if (!isStrict) {
72-
// Skip assignability check below, but continue to validate remaining args
73-
continue;
74-
}
75-
}
78+
static boolean canHandle(Object arg, ImmutableList<Class<?>> parameterTypes, boolean isStrict) {
79+
if (parameterTypes.size() != 1) {
80+
return false;
81+
}
82+
return canHandleArg(arg, parameterTypes.get(0), isStrict);
83+
}
84+
85+
static boolean canHandle(
86+
Object arg1, Object arg2, ImmutableList<Class<?>> parameterTypes, boolean isStrict) {
87+
if (parameterTypes.size() != 2) {
88+
return false;
89+
}
90+
return canHandleArg(arg1, parameterTypes.get(0), isStrict)
91+
&& canHandleArg(arg2, parameterTypes.get(1), isStrict);
92+
}
7693

77-
if (!paramType.isAssignableFrom(arg.getClass())) {
94+
static boolean canHandleArg(Object arg, Class<?> paramType, boolean isStrict) {
95+
// null can be assigned to messages, maps, and to objects.
96+
// TODO: Remove null special casing
97+
if (arg == null) {
98+
if (paramType != Object.class && !Map.class.isAssignableFrom(paramType)) {
7899
return false;
79100
}
101+
return true;
80102
}
81-
return true;
103+
104+
if (arg instanceof Exception || arg instanceof CelUnknownSet) {
105+
if (!isStrict) {
106+
return true;
107+
}
108+
}
109+
110+
return paramType.isAssignableFrom(arg.getClass());
82111
}
83112
}

runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -200,19 +200,48 @@ public DefaultDispatcher build() {
200200
for (Map.Entry<String, OverloadEntry> entry : overloads.entrySet()) {
201201
String overloadId = entry.getKey();
202202
OverloadEntry overloadEntry = entry.getValue();
203+
CelFunctionOverload overloadImpl = overloadEntry.overload();
204+
205+
CelFunctionOverload guardedApply;
206+
if (overloadImpl instanceof DynamicDispatchOverload) {
207+
// Dynamic dispatcher already does its own internal canHandle checks
208+
guardedApply = overloadImpl;
209+
} else {
210+
boolean isStrict = overloadEntry.isStrict();
211+
ImmutableList<Class<?>> argTypes = overloadEntry.argTypes();
212+
213+
guardedApply =
214+
new CelFunctionOverload() {
215+
@Override
216+
public Object apply(Object[] args) throws CelEvaluationException {
217+
if (CelFunctionOverload.canHandle(args, argTypes, isStrict)) {
218+
return overloadImpl.apply(args);
219+
}
220+
throw new CelOverloadNotFoundException(overloadId);
221+
}
222+
223+
@Override
224+
public Object apply(Object arg) throws CelEvaluationException {
225+
if (CelFunctionOverload.canHandle(arg, argTypes, isStrict)) {
226+
return overloadImpl.apply(arg);
227+
}
228+
throw new CelOverloadNotFoundException(overloadId);
229+
}
230+
231+
@Override
232+
public Object apply(Object arg1, Object arg2) throws CelEvaluationException {
233+
if (CelFunctionOverload.canHandle(arg1, arg2, argTypes, isStrict)) {
234+
return overloadImpl.apply(arg1, arg2);
235+
}
236+
throw new CelOverloadNotFoundException(overloadId);
237+
}
238+
};
239+
}
240+
203241
resolvedOverloads.put(
204242
overloadId,
205243
CelResolvedOverload.of(
206-
overloadId,
207-
args ->
208-
guardedOp(
209-
overloadId,
210-
args,
211-
overloadEntry.argTypes(),
212-
overloadEntry.isStrict(),
213-
overloadEntry.overload()),
214-
overloadEntry.isStrict(),
215-
overloadEntry.argTypes()));
244+
overloadId, guardedApply, overloadEntry.isStrict(), overloadEntry.argTypes()));
216245
}
217246

218247
return new DefaultDispatcher(resolvedOverloads.buildOrThrow());
@@ -223,23 +252,6 @@ private Builder() {
223252
}
224253
}
225254

226-
/** Creates an invocation guard around the overload definition. */
227-
private static Object guardedOp(
228-
String functionName,
229-
Object[] args,
230-
ImmutableList<Class<?>> argTypes,
231-
boolean isStrict,
232-
CelFunctionOverload overload)
233-
throws CelEvaluationException {
234-
// Argument checking for DynamicDispatch is handled inside the overload's apply method itself.
235-
if (overload instanceof DynamicDispatchOverload
236-
|| CelFunctionOverload.canHandle(args, argTypes, isStrict)) {
237-
return overload.apply(args);
238-
}
239-
240-
throw new CelOverloadNotFoundException(functionName);
241-
}
242-
243255
DefaultDispatcher(ImmutableMap<String, CelResolvedOverload> overloads) {
244256
this.overloads = overloads;
245257
}

runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ java_library(
1717
":error_metadata",
1818
":eval_and",
1919
":eval_attribute",
20+
":eval_binary",
2021
":eval_conditional",
2122
":eval_const",
2223
":eval_create_list",
@@ -232,6 +233,21 @@ java_library(
232233
],
233234
)
234235

236+
java_library(
237+
name = "eval_binary",
238+
srcs = ["EvalBinary.java"],
239+
deps = [
240+
":eval_helpers",
241+
":execution_frame",
242+
":planned_interpretable",
243+
"//common/values",
244+
"//runtime:accumulated_unknowns",
245+
"//runtime:evaluation_exception",
246+
"//runtime:interpretable",
247+
"//runtime:resolved_overload",
248+
],
249+
)
250+
235251
java_library(
236252
name = "eval_var_args_call",
237253
srcs = ["EvalVarArgsCall.java"],
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly;
18+
import static dev.cel.runtime.planner.EvalHelpers.evalStrictly;
19+
20+
import dev.cel.common.values.CelValueConverter;
21+
import dev.cel.runtime.AccumulatedUnknowns;
22+
import dev.cel.runtime.CelEvaluationException;
23+
import dev.cel.runtime.CelResolvedOverload;
24+
import dev.cel.runtime.GlobalResolver;
25+
26+
final class EvalBinary extends PlannedInterpretable {
27+
28+
private final CelResolvedOverload resolvedOverload;
29+
private final PlannedInterpretable arg1;
30+
private final PlannedInterpretable arg2;
31+
private final CelValueConverter celValueConverter;
32+
33+
@Override
34+
public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException {
35+
Object argVal1 =
36+
resolvedOverload.isStrict()
37+
? evalStrictly(arg1, resolver, frame)
38+
: evalNonstrictly(arg1, resolver, frame);
39+
Object argVal2 =
40+
resolvedOverload.isStrict()
41+
? evalStrictly(arg2, resolver, frame)
42+
: evalNonstrictly(arg2, resolver, frame);
43+
44+
AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1);
45+
unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2);
46+
47+
if (unknowns != null) {
48+
return unknowns;
49+
}
50+
51+
return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal1, argVal2);
52+
}
53+
54+
static EvalBinary create(
55+
long exprId,
56+
CelResolvedOverload resolvedOverload,
57+
PlannedInterpretable arg1,
58+
PlannedInterpretable arg2,
59+
CelValueConverter celValueConverter) {
60+
return new EvalBinary(exprId, resolvedOverload, arg1, arg2, celValueConverter);
61+
}
62+
63+
private EvalBinary(
64+
long exprId,
65+
CelResolvedOverload resolvedOverload,
66+
PlannedInterpretable arg1,
67+
PlannedInterpretable arg2,
68+
CelValueConverter celValueConverter) {
69+
super(exprId);
70+
this.resolvedOverload = resolvedOverload;
71+
this.arg1 = arg1;
72+
this.arg2 = arg2;
73+
this.celValueConverter = celValueConverter;
74+
}
75+
}

runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,44 @@ static Object dispatch(
6161
try {
6262
Object result = overload.getDefinition().apply(args);
6363
return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result));
64-
} catch (CelRuntimeException e) {
65-
// Function dispatch failure that's already been handled -- just propagate.
66-
throw e;
6764
} catch (RuntimeException e) {
68-
// Unexpected function dispatch failure.
69-
throw new IllegalArgumentException(
70-
String.format(
71-
"Function '%s' failed with arg(s) '%s'",
72-
overload.getOverloadId(), Joiner.on(", ").join(args)),
73-
e);
65+
throw handleDispatchException(e, overload, args);
66+
}
67+
}
68+
69+
static Object dispatch(CelResolvedOverload overload, CelValueConverter valueConverter, Object arg)
70+
throws CelEvaluationException {
71+
try {
72+
Object result = overload.getDefinition().apply(arg);
73+
return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result));
74+
} catch (RuntimeException e) {
75+
throw handleDispatchException(e, overload, arg);
76+
}
77+
}
78+
79+
static Object dispatch(
80+
CelResolvedOverload overload, CelValueConverter valueConverter, Object arg1, Object arg2)
81+
throws CelEvaluationException {
82+
try {
83+
Object result = overload.getDefinition().apply(arg1, arg2);
84+
return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result));
85+
} catch (RuntimeException e) {
86+
throw handleDispatchException(e, overload, arg1, arg2);
87+
}
88+
}
89+
90+
private static RuntimeException handleDispatchException(
91+
RuntimeException e, CelResolvedOverload overload, Object... args) {
92+
if (e instanceof CelRuntimeException) {
93+
// Function dispatch failure that's already been handled -- just propagate.
94+
return e;
7495
}
96+
// Unexpected function dispatch failure.
97+
return new IllegalArgumentException(
98+
String.format(
99+
"Function '%s' failed with arg(s) '%s'",
100+
overload.getOverloadId(), Joiner.on(", ").join(args)),
101+
e);
75102
}
76103

77104
private EvalHelpers() {}

runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval
3434
resolvedOverload.isStrict()
3535
? evalStrictly(arg, resolver, frame)
3636
: evalNonstrictly(arg, resolver, frame);
37-
Object[] arguments = new Object[] {argVal};
38-
39-
return EvalHelpers.dispatch(resolvedOverload, celValueConverter, arguments);
37+
return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal);
4038
}
4139

4240
static EvalUnary create(

runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) {
276276
return EvalZeroArity.create(expr.id(), resolvedOverload, celValueConverter);
277277
case 1:
278278
return EvalUnary.create(expr.id(), resolvedOverload, evaluatedArgs[0], celValueConverter);
279+
case 2:
280+
return EvalBinary.create(
281+
expr.id(), resolvedOverload, evaluatedArgs[0], evaluatedArgs[1], celValueConverter);
279282
default:
280283
return EvalVarArgsCall.create(
281284
expr.id(), resolvedOverload, evaluatedArgs, celValueConverter);

0 commit comments

Comments
 (0)