Skip to content

Commit da837a3

Browse files
committed
performance comparison
1 parent e8671e9 commit da837a3

4 files changed

Lines changed: 290 additions & 1 deletion

File tree

1.88 KB
Binary file not shown.
6.07 KB
Binary file not shown.
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/*
2+
* @test
3+
* @summary Measure impact of skipping hasEffectiveAnnotation(NONNULL) fast-path on many `new` sites.
4+
*
5+
* @run NewClassPerf
6+
*/
7+
8+
import java.io.BufferedWriter;
9+
import java.io.File;
10+
import java.io.FileWriter;
11+
import java.io.IOException;
12+
import java.nio.charset.StandardCharsets;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.nio.file.Paths;
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
public class NewClassPerf {
22+
23+
private static final int RUNS = Integer.getInteger("perf.runs", 10);
24+
private static final int GROUPS = Integer.getInteger("perf.groups", 400); // ~2k new-exprs total (5 per group)
25+
// Protocol: AB | BA | BOTH | SEPARATE. Default BOTH runs interleaved AB and BA to cancel order bias.
26+
private static final String PROTOCOL = System.getProperty("perf.protocol", "BOTH");
27+
private static final int WARMUP = Integer.getInteger("perf.warmupPerVariant", 1);
28+
29+
public static void main(String[] args) throws Exception {
30+
Path workDir = Paths.get(".").toAbsolutePath().normalize();
31+
Path src = workDir.resolve("ManyNew.java");
32+
writeManyNewSource(src, GROUPS);
33+
34+
switch (PROTOCOL.toUpperCase()) {
35+
case "AB": {
36+
Result[] ab = timeInterleaved(src, "AB");
37+
printResults("Interleaved AB", ab[0], ab[1]);
38+
break;
39+
}
40+
case "BA": {
41+
Result[] ba = timeInterleaved(src, "BA");
42+
printResults("Interleaved BA", ba[0], ba[1]);
43+
break;
44+
}
45+
case "SEPARATE": {
46+
Result[] sep = timeSeparate(src, WARMUP);
47+
printResults("Separate (warmup=" + WARMUP + ")", sep[0], sep[1]);
48+
break;
49+
}
50+
case "BOTH":
51+
default: {
52+
Result[] ab = timeInterleaved(src, "AB");
53+
Result[] ba = timeInterleaved(src, "BA");
54+
System.out.println("==== Interleaved AB ====");
55+
printResults("AB", ab[0], ab[1]);
56+
System.out.println();
57+
System.out.println("==== Interleaved BA ====");
58+
printResults("BA", ba[0], ba[1]);
59+
// Consistency check
60+
double sign1 = Math.signum((ab[1].median() - ab[0].median()));
61+
double sign2 = Math.signum((ba[1].median() - ba[0].median()));
62+
System.out.println();
63+
if (sign1 == 0 || sign2 == 0 || Math.signum(sign1) != Math.signum(sign2)) {
64+
System.out.println("Direction: ORDER-SENSITIVE (results differ between AB and BA)");
65+
} else {
66+
System.out.println("Direction: CONSISTENT across AB and BA");
67+
}
68+
break;
69+
}
70+
}
71+
}
72+
73+
private static void writeManyNewSource(Path file, int groups) throws IOException {
74+
StringBuilder sb = new StringBuilder(1024 * 1024);
75+
sb.append("import java.util.*;\n");
76+
sb.append("public class ManyNew {\n");
77+
// Avoid initialization checker errors; use @Nullable type variable upper bound.
78+
sb.append(" static class Box<T extends Object> { T t; Box(){ this.t = (T) (Object) new Object(); } }\n");
79+
sb.append(" void f() {\n");
80+
for (int i = 0; i < groups; i++) {
81+
sb.append(" Object o").append(i).append(" = new Object();\n");
82+
sb.append(" ArrayList<String> l").append(i).append(" = new ArrayList<>();\n");
83+
sb.append(" Box<String> b").append(i).append(" = new Box<>();\n");
84+
sb.append(" int[] ai").append(i).append(" = new int[10];\n");
85+
sb.append(" String[] as").append(i).append(" = new String[10];\n");
86+
sb.append(" Runnable r").append(i)
87+
.append(" = new Runnable(){ public void run(){} };\n");
88+
}
89+
sb.append(" }\n");
90+
sb.append("}\n");
91+
try (BufferedWriter w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
92+
w.write(sb.toString());
93+
}
94+
}
95+
96+
private static Result timeVariant(Path src, boolean skipFastPath) throws Exception {
97+
List<Long> timings = new ArrayList<>();
98+
for (int i = 0; i < RUNS; i++) {
99+
timings.add(runOnceMs(src, skipFastPath));
100+
}
101+
return new Result(timings);
102+
}
103+
104+
private static Result[] timeInterleaved(Path src, String order) throws Exception {
105+
List<Long> aTimes = new ArrayList<>();
106+
List<Long> bTimes = new ArrayList<>();
107+
boolean firstA = !"BA".equalsIgnoreCase(order);
108+
for (int i = 0; i < RUNS; i++) {
109+
if (firstA) {
110+
aTimes.add(runOnceMs(src, /*skipFastPath=*/false));
111+
bTimes.add(runOnceMs(src, /*skipFastPath=*/true));
112+
} else {
113+
bTimes.add(runOnceMs(src, /*skipFastPath=*/true));
114+
aTimes.add(runOnceMs(src, /*skipFastPath=*/false));
115+
}
116+
}
117+
return new Result[] { new Result(aTimes), new Result(bTimes) };
118+
}
119+
120+
private static long runOnceMs(Path src, boolean skipFastPath) throws Exception {
121+
long start = System.nanoTime();
122+
int exit = runJavac(src, skipFastPath);
123+
long end = System.nanoTime();
124+
if (exit != 0) {
125+
throw new RuntimeException("javac failed with exit code " + exit);
126+
}
127+
return (end - start) / 1_000_000L;
128+
}
129+
130+
private static Result[] timeSeparate(Path src, int warmup) throws Exception {
131+
// Variant A (fast-path enabled)
132+
for (int i = 0; i < warmup; i++) {
133+
runOnceMs(src, /*skipFastPath=*/false);
134+
}
135+
List<Long> aTimes = new ArrayList<>();
136+
for (int i = 0; i < RUNS; i++) {
137+
aTimes.add(runOnceMs(src, /*skipFastPath=*/false));
138+
}
139+
140+
// Variant B (fast-path disabled)
141+
for (int i = 0; i < warmup; i++) {
142+
runOnceMs(src, /*skipFastPath=*/true);
143+
}
144+
List<Long> bTimes = new ArrayList<>();
145+
for (int i = 0; i < RUNS; i++) {
146+
bTimes.add(runOnceMs(src, /*skipFastPath=*/true));
147+
}
148+
return new Result[] { new Result(aTimes), new Result(bTimes) };
149+
}
150+
151+
private static void printResults(String label, Result a, Result b) {
152+
// Print raw timings
153+
System.out.println("Variant A (fast-path enabled) timings ms: " + a.timingsMs);
154+
System.out.println("Variant B (fast-path disabled) timings ms: " + b.timingsMs);
155+
156+
// Summary table and deltas
157+
System.out.println();
158+
System.out.println("Results (ms) - " + label + ":");
159+
System.out.println("Variant | median | average");
160+
System.out.println(String.format("A | %7.2f | %7.2f", a.median(), a.average()));
161+
System.out.println(String.format("B | %7.2f | %7.2f", b.median(), b.average()));
162+
double medA = a.median(), medB = b.median();
163+
double avgA = a.average(), avgB = b.average();
164+
System.out.println();
165+
System.out.printf("Median delta (B vs A): %.3f%%%n", (medB - medA) / medA * 100.0);
166+
System.out.printf("Average delta (B vs A): %.3f%%%n", (avgB - avgA) / avgA * 100.0);
167+
}
168+
169+
private static int runJavac(Path src, boolean skipFastPath) throws Exception {
170+
String checkerJar = locateCheckerJar();
171+
172+
List<String> cmd = new ArrayList<>();
173+
cmd.add(findJavacExecutable());
174+
// Add required --add-opens for running Checker Framework on JDK 9+
175+
String[] javacPkgs = new String[] {
176+
"com.sun.tools.javac.api",
177+
"com.sun.tools.javac.code",
178+
"com.sun.tools.javac.comp",
179+
"com.sun.tools.javac.file",
180+
"com.sun.tools.javac.main",
181+
"com.sun.tools.javac.parser",
182+
"com.sun.tools.javac.processing",
183+
"com.sun.tools.javac.tree",
184+
"com.sun.tools.javac.util"
185+
};
186+
for (String p : javacPkgs) {
187+
cmd.add("-J--add-opens=jdk.compiler/" + p + "=ALL-UNNAMED");
188+
}
189+
if (skipFastPath) {
190+
cmd.add("-J-Dcf.skipNonnullFastPath=true");
191+
}
192+
cmd.addAll(Arrays.asList(
193+
"-classpath", checkerJar,
194+
"-processor", "org.checkerframework.checker.nullness.NullnessChecker",
195+
"-proc:only",
196+
"-source", "8",
197+
"-target", "8",
198+
"-Xlint:-options",
199+
src.getFileName().toString()));
200+
201+
ProcessBuilder pb = new ProcessBuilder(cmd);
202+
pb.directory(src.getParent().toFile());
203+
pb.redirectErrorStream(true);
204+
Process p = pb.start();
205+
// Consume output to avoid buffer blockage.
206+
byte[] out = p.getInputStream().readAllBytes();
207+
int code = p.waitFor();
208+
// In case of failure, print compiler output for debugging.
209+
if (code != 0) {
210+
System.out.write(out);
211+
}
212+
return code;
213+
}
214+
215+
private static String locateCheckerJar() {
216+
try {
217+
String testRoot = System.getProperty("test.root");
218+
if (testRoot != null) {
219+
Path p = Paths.get(testRoot).toAbsolutePath().normalize().getParent().resolve("dist/checker.jar");
220+
if (Files.exists(p)) {
221+
return p.toString();
222+
}
223+
}
224+
Path p1 = Paths.get("checker/dist/checker.jar").toAbsolutePath().normalize();
225+
if (Files.exists(p1)) {
226+
return p1.toString();
227+
}
228+
Path p2 = Paths.get("../../../dist/checker.jar").toAbsolutePath().normalize();
229+
if (Files.exists(p2)) {
230+
return p2.toString();
231+
}
232+
// Fallback: walk up from the current working dir (jtreg scratch)
233+
Path cwd = Paths.get(".").toAbsolutePath().normalize();
234+
for (int i = 0; i < 8; i++) {
235+
Path cand = cwd;
236+
for (int j = 0; j < i; j++) cand = cand.getParent();
237+
if (cand == null) break;
238+
Path jar = cand.resolve("checker/dist/checker.jar");
239+
if (Files.exists(jar)) return jar.toString();
240+
}
241+
} catch (Throwable ignore) {
242+
}
243+
return "checker/dist/checker.jar";
244+
}
245+
246+
private static String findJavacExecutable() {
247+
String javaHome = System.getProperty("java.home");
248+
// Typical JDK layout: $JAVA_HOME/bin/javac
249+
File jh = new File(javaHome);
250+
File bin = new File(jh.getParentFile(), "bin");
251+
File javac = new File(bin, isWindows() ? "javac.exe" : "javac");
252+
if (javac.exists()) {
253+
return javac.getAbsolutePath();
254+
}
255+
// Fallback to PATH
256+
return isWindows() ? "javac.exe" : "javac";
257+
}
258+
259+
private static boolean isWindows() {
260+
String os = System.getProperty("os.name", "").toLowerCase();
261+
return os.contains("win");
262+
}
263+
264+
private static final class Result {
265+
final List<Long> timingsMs;
266+
Result(List<Long> t) { this.timingsMs = Collections.unmodifiableList(new ArrayList<>(t)); }
267+
double average() { return timingsMs.stream().mapToLong(Long::longValue).average().orElse(0); }
268+
double median() {
269+
if (timingsMs.isEmpty()) return 0;
270+
List<Long> copy = new ArrayList<>(timingsMs);
271+
Collections.sort(copy);
272+
int n = copy.size();
273+
if ((n & 1) == 1) {
274+
return copy.get(n/2);
275+
} else {
276+
return (copy.get(n/2 - 1) + copy.get(n/2)) / 2.0;
277+
}
278+
}
279+
}
280+
}
281+
282+

checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFactory.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ public class NullnessNoInitAnnotatedTypeFactory
7979
NullnessNoInitTransfer,
8080
NullnessNoInitAnalysis> {
8181

82+
/**
83+
* Runtime toggle: skip the {@code hasEffectiveAnnotation(NONNULL)} fast-path. Controlled via
84+
* JVM system property {@code -Dcf.skipNonnullFastPath=true}.
85+
*/
86+
private static final boolean SKIP_NONNULL_FASTPATH =
87+
Boolean.getBoolean("cf.skipNonnullFastPath");
88+
8289
/** The @{@link NonNull} annotation. */
8390
protected final AnnotationMirror NONNULL = AnnotationBuilder.fromClass(elements, NonNull.class);
8491

@@ -734,7 +741,7 @@ public Void visitUnary(UnaryTree tree, AnnotatedTypeMirror type) {
734741
// explicit nullable annotations are left intact for the visitor to inspect.
735742
@Override
736743
public Void visitNewClass(NewClassTree tree, AnnotatedTypeMirror type) {
737-
if (type.hasEffectiveAnnotation(NONNULL)) {
744+
if (!SKIP_NONNULL_FASTPATH && type.hasEffectiveAnnotation(NONNULL)) {
738745
return null;
739746
}
740747
type.addMissingAnnotation(NONNULL);

0 commit comments

Comments
 (0)