Betrachten Sie die folgenden zwei Codeausschnitte in einem Array der Länge 2:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
und
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
Ich würde annehmen, dass die Leistung dieser beiden Stücke nach ausreichendem Aufwärmen ähnlich sein sollte.
Ich habe dies mit dem JMH-Mikro-Benchmarking-Framework überprüft, wie z. B. hier und hier beschrieben, und festgestellt, dass das zweite Snippet mehr als 10% schneller ist.
Frage: Warum hat Java mein erstes Snippet nicht mithilfe der grundlegenden Technik zum Abrollen von Schleifen optimiert?
Insbesondere möchte ich Folgendes verstehen:
- Ich kann leicht einen Code erstellen, der für Fälle von 2 Filtern optimal ist und bei einer anderen Anzahl von Filtern trotzdem funktioniert (stellen Sie sich einen einfachen Builder vor) :
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)
. Kann JITC dasselbe tun und wenn nicht, warum? - Kann JITC erkennen, dass ' filter.length == 2 ' der häufigste Fall ist, und nach einer gewissen Aufwärmphase den für diesen Fall optimalen Code erzeugen? Dies sollte fast so optimal sein wie die manuell abgewickelte Version.
- Kann JITC erkennen, dass eine bestimmte Instanz sehr häufig verwendet wird, und dann einen Code für diese bestimmte Instanz erstellen (für die bekannt ist, dass die Anzahl der Filter immer 2 beträgt)?
Update: Ich habe die Antwort erhalten, dass JITC nur auf Klassenebene funktioniert. OK habe es.
Im Idealfall möchte ich eine Antwort von jemandem erhalten, der ein tiefes Verständnis für die Funktionsweise von JITC hat.
Details zum Benchmark-Lauf:
- Bei den neuesten Versionen von Java 8 OpenJDK und Oracle HotSpot wurden ähnliche Ergebnisse erzielt
- Verwendete Java-Flags: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (ähnliche Ergebnisse auch ohne die ausgefallenen Flags erhalten)
- Übrigens bekomme ich ein ähnliches Laufzeitverhältnis, wenn ich es einfach mehrere Milliarden Mal in einer Schleife ausführe (nicht über JMH), dh das zweite Snippet ist immer deutlich schneller
Typische Benchmark-Ausgabe:
Benchmark (filterIndex) -Modus Cnt-Score-
Fehlereinheiten LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44,202 ± 0,224 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38,347 ± 0,063 ns / op
(Die erste Zeile entspricht dem ersten Snippet, die zweite Zeile - der zweiten.
Vollständiger Benchmark-Code:
public class LoopUnrollingBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkData {
public Filter[] filters;
@Param({"0", "1"})
public int filterIndex;
public int num;
@Setup(Level.Invocation) //similar ratio with Level.TRIAL
public void setUp() {
filters = new Filter[]{new FilterChain1(), new FilterChain2()};
num = new Random().nextInt();
}
}
@Benchmark
@Fork(warmups = 5, value = 20)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public int runBenchmark(BenchmarkData data) {
Filter filter = data.filters[data.filterIndex];
int sum = 0;
int num = data.num;
if (filter.isOK(num)) {
++sum;
}
if (filter.isOK(num + 1)) {
++sum;
}
if (filter.isOK(num - 1)) {
++sum;
}
if (filter.isOK(num * 2)) {
++sum;
}
if (filter.isOK(num * 3)) {
++sum;
}
if (filter.isOK(num * 5)) {
++sum;
}
return sum;
}
interface Filter {
boolean isOK(int i);
}
static class Filter1 implements Filter {
@Override
public boolean isOK(int i) {
return i % 3 == 1;
}
}
static class Filter2 implements Filter {
@Override
public boolean isOK(int i) {
return i % 7 == 3;
}
}
static class FilterChain1 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
}
static class FilterChain2 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
}
private static Filter[] createLeafFilters() {
Filter[] filters = new Filter[2];
filters[0] = new Filter1();
filters[1] = new Filter2();
return filters;
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
@Setup(Level.Invocation)
: nicht sicher, ob es hilft (siehe Javadoc).
final
, aber JIT sieht nicht, dass alle Instanzen der Klasse ein Array der Länge 2 erhalten. Um dies zu sehen, müsste es in das Feld eintauchen createLeafFilters()
Methode und analysieren Sie den Code tief genug, um zu erfahren, dass das Array immer 2 lang sein wird. Warum sollte der JIT-Optimierer Ihrer Meinung nach so tief in Ihren Code eintauchen?