Skip to content

Commit aded06b

Browse files
fmeumcopybara-github
authored andcommitted
Starlark: reuse positional array in native calls where possible
This is a resubmission of @stepancheg's #12782, which became stale, with this original description: ==== For certain calls like `type(x)` or `str(x)` or `l.append(x)` we can reuse `positional` array argument of `fastcall` to pass it to the `MethodDescriptor.call` and skip all runtime checks in `BuiltinFunction`. This optimization cannot be applied if * a function has extra positional or named arguments * has arguments guarded by flag * accepts extra `StarlarkThread` parameter * has unfilled arguments with default values So this optimation won't be used for calls `list(x)` or `bool()`. For `type(1)` called in loop, the result is 15% speedup: ``` A: n=30 mean=5.705 std=0.387 se=0.071 min=5.131 med=5.827 B: n=30 mean=4.860 std=0.345 se=0.064 min=4.396 med=4.946 B/A: 0.852 0.820..0.884 (95% conf) ``` For `bool()` (no argument) called in loop, it results in 1% slowdown: ``` A: n=29 mean=9.045 std=0.603 se=0.113 min=8.096 med=9.400 B: n=29 mean=9.134 std=0.520 se=0.098 min=8.248 med=9.448 B/A: 1.010 0.976..1.045 (95% conf) ``` ==== Beyond the original PR, this includes benchmarks, a test case addressing a review comment and small adaptions to the implementation to make it work with recent changes. Benchmark results with JDK 21 and `--seconds 5`: ``` before: File src/test/java/net/starlark/java/eval/testdata/bench_call.star: benchmark ops cpu/op wall/op steps/op alloc/op bench_call_bool 67108863 100ns 100ns 3 87B bench_call_dict_get 33554431 159ns 158ns 5 143B bench_call_dict_get_none 33554431 180ns 179ns 6 143B bench_call_list_append 33554431 170ns 169ns 5 207B bench_call_type 67108863 139ns 138ns 4 111B after: File src/test/java/net/starlark/java/eval/testdata/bench_call.star: benchmark ops cpu/op wall/op steps/op alloc/op bench_call_bool 67108863 100ns 100ns 3 87B bench_call_dict_get 33554431 155ns 154ns 5 143B bench_call_dict_get_none 33554431 174ns 174ns 6 143B bench_call_list_append 67108863 141ns 141ns 5 183B bench_call_type 67108863 115ns 114ns 4 87B ``` Closes #19708. PiperOrigin-RevId: 602821608 Change-Id: If40e2eb1e09ec43d56b36ba13987aa5312fd47ec
1 parent e57d93f commit aded06b

File tree

4 files changed

+90
-2
lines changed

4 files changed

+90
-2
lines changed

src/main/java/net/starlark/java/eval/BuiltinFunction.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,11 @@ public String toString() {
128128
* @param thread the Starlark thread for the call
129129
* @param loc the location of the call expression, or BUILTIN for calls from Java
130130
* @param desc descriptor for the StarlarkMethod-annotated method
131-
* @param positional a list of positional arguments
131+
* @param positional an array of positional arguments; as an optimization, in simple cases, this
132+
* array may be reused as the method's return value
132133
* @param named a list of named arguments, as alternating Strings/Objects. May contain dups.
133-
* @return the array of arguments which may be passed to {@link MethodDescriptor#call}
134+
* @return the array of arguments which may be passed to {@link MethodDescriptor#call}. It is
135+
* unsafe to mutate the returned array.
134136
* @throws EvalException if the given set of arguments are invalid for the given method. For
135137
* example, if any arguments are of unexpected type, or not all mandatory parameters are
136138
* specified by the user
@@ -156,6 +158,16 @@ private Object[] getArgumentVector(
156158

157159
ParamDescriptor[] parameters = desc.getParameters();
158160

161+
// Fast case: we only accept positionals and can reuse `positional` as the Java args vector.
162+
// Note that StringModule methods, which are treated specially below, will never match this case
163+
// since their `self` parameter is restricted to String and thus
164+
// isPositionalsReusableAsCallArgsVectorIfArgumentCountValid() would be false.
165+
if (desc.isPositionalsReusableAsJavaArgsVectorIfArgumentCountValid()
166+
&& positional.length == parameters.length
167+
&& named.length == 0) {
168+
return positional;
169+
}
170+
159171
// Allocate argument vector.
160172
int n = parameters.length;
161173
if (desc.acceptsExtraArgs()) {

src/main/java/net/starlark/java/eval/MethodDescriptor.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package net.starlark.java.eval;
1616

17+
import static java.util.Arrays.stream;
18+
1719
import com.google.common.base.Preconditions;
1820
import com.google.common.base.Throwables;
1921
import com.google.errorprone.annotations.CheckReturnValue;
@@ -46,6 +48,7 @@ final class MethodDescriptor {
4648
private final boolean allowReturnNones;
4749
private final boolean useStarlarkThread;
4850
private final boolean useStarlarkSemantics;
51+
private final boolean positionalsReusableAsJavaArgsVectorIfArgumentCountValid;
4952

5053
private enum HowToHandleReturn {
5154
NULL_TO_NONE, // any Starlark value; null -> None
@@ -100,6 +103,19 @@ private MethodDescriptor(
100103
} else {
101104
howToHandleReturn = HowToHandleReturn.FROM_JAVA;
102105
}
106+
107+
this.positionalsReusableAsJavaArgsVectorIfArgumentCountValid =
108+
!extraKeywords
109+
&& !extraPositionals
110+
&& !useStarlarkSemantics
111+
&& !useStarlarkThread
112+
&& stream(parameters).allMatch(MethodDescriptor::paramUsableAsPositionalWithoutChecks);
113+
}
114+
115+
private static boolean paramUsableAsPositionalWithoutChecks(ParamDescriptor param) {
116+
return param.isPositional()
117+
&& param.disabledByFlag() == null
118+
&& param.getAllowedClasses() == null;
103119
}
104120

105121
/** Returns the StarlarkMethod annotation corresponding to this method. */
@@ -287,4 +303,18 @@ String getDoc() {
287303
boolean isSelfCall() {
288304
return selfCall;
289305
}
306+
307+
/**
308+
* Returns true if we may directly reuse the Starlark positionals vector as the Java {@code args}
309+
* vector passed to {@link #call} as long as the Starlark call was made with a valid number of
310+
* arguments.
311+
*
312+
* <p>More precisely, this means that we do not need to insert extra values into the args vector
313+
* (such as ones corresponding to {@code *args}, {@code **kwargs}, or {@code self} in Starlark),
314+
* and all Starlark parameters are simple positional parameters which cannot be disabled by a flag
315+
* and do not require type checking.
316+
*/
317+
boolean isPositionalsReusableAsJavaArgsVectorIfArgumentCountValid() {
318+
return positionalsReusableAsJavaArgsVectorIfArgumentCountValid;
319+
}
290320
}

src/test/java/net/starlark/java/eval/MethodLibraryTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import com.google.common.collect.ImmutableCollection;
2323
import com.google.common.collect.ImmutableList;
2424
import java.util.List;
25+
import net.starlark.java.annot.Param;
2526
import net.starlark.java.annot.StarlarkBuiltin;
27+
import net.starlark.java.annot.StarlarkMethod;
2628
import org.junit.Test;
2729
import org.junit.runner.RunWith;
2830
import org.junit.runners.JUnit4;
@@ -772,6 +774,29 @@ ev.new Scenario()
772774
"','.join(elements=['foo', 'bar'])");
773775
}
774776

777+
@StarlarkBuiltin(name = "named_only", doc = "")
778+
static final class NamedOnly implements StarlarkValue {
779+
@StarlarkMethod(
780+
name = "foo",
781+
documented = false,
782+
parameters = {
783+
@Param(name = "a"),
784+
@Param(name = "b", named = true, defaultValue = "None", positional = false),
785+
})
786+
public Object foo(Object a, Object b) {
787+
return a;
788+
}
789+
}
790+
791+
@Test
792+
public void testNamedOnlyArgument() throws Exception {
793+
ev.new Scenario()
794+
.update("named_only", new NamedOnly())
795+
.testIfErrorContains(
796+
"foo() accepts no more than 1 positional argument but got 2",
797+
"named_only.foo([1, 2, 3], int)");
798+
}
799+
775800
@Test
776801
public void testStringJoinRequiresStrings() throws Exception {
777802
ev.new Scenario()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
def bench_call_type(b):
2+
for _ in range(b.n):
3+
type(1)
4+
5+
def bench_call_list_append(b):
6+
for _ in range(b.n):
7+
[].append("foo")
8+
9+
def bench_call_dict_get(b):
10+
d = {"foo": "bar"}
11+
for _ in range(b.n):
12+
d.get("baz")
13+
14+
def bench_call_dict_get_none(b):
15+
d = {"foo": "bar"}
16+
for _ in range(b.n):
17+
d.get("baz", None)
18+
19+
def bench_call_bool(b):
20+
for _ in range(b.n):
21+
bool()

0 commit comments

Comments
 (0)