Considera i seguenti due frammenti di codice su un array di lunghezza 2:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
e
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
Suppongo che le prestazioni di questi due pezzi dovrebbero essere simili dopo un riscaldamento sufficiente.
L'ho verificato utilizzando il framework di micro-benchmarking JMH come descritto ad esempio qui e qui e ho osservato che il secondo frammento è più veloce del 10%.
Domanda: perché Java non ha ottimizzato il mio primo frammento utilizzando la tecnica di srotolamento del ciclo di base?
In particolare, vorrei comprendere quanto segue:
- Posso facilmente produrre un codice che è ottimale per i casi di 2 filtri ed ancora può funzionare in caso di un altro numero di filtri (immaginate un semplice costruttore):
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters). JITC può fare lo stesso e, in caso contrario, perché? - JITC è in grado di rilevare che " filters.length == 2 " è il caso più frequente e di produrre il codice ottimale per questo caso dopo un po 'di riscaldamento? Questo dovrebbe essere quasi ottimale come la versione srotolata manualmente.
- JITC è in grado di rilevare che una particolare istanza viene utilizzata molto frequentemente e quindi produrre un codice per questa specifica istanza (per la quale sa che il numero di filtri è sempre 2)?
Aggiornamento: ho ottenuto la risposta che JITC funziona solo a livello di classe. Ok capito.
Idealmente, vorrei ricevere una risposta da qualcuno con una profonda comprensione di come funziona JITC.
Dettagli della corsa di riferimento:
- Provato con le ultime versioni di Java 8 OpenJDK e Oracle HotSpot, i risultati sono simili
- Flag Java usati: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (ottenuto risultati simili anche senza i flag di fantasia)
- A proposito, ottengo un rapporto di runtime simile se lo eseguo semplicemente diversi miliardi di volte in un ciclo (non tramite JMH), ovvero il secondo frammento è sempre chiaramente più veloce
Uscita di riferimento tipica:
Benchmark (filterIndex) Modalità Cnt Punteggio Errore Unità
LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44,202 ± 0,224 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38,347 ± 0,063 ns / op
(La prima riga corrisponde al primo frammento, la seconda riga - alla seconda.
Codice di riferimento completo:
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): non sono sicuro che aiuti (vedi il javadoc).
final, ma JIT non vede che tutte le istanze della classe otterranno un array di lunghezza 2. Per vederlo, dovrebbe immergersi nel createLeafFilters()metodo e analizzare il codice abbastanza in profondità per apprendere che l'array sarà sempre lungo 2. Perché ritieni che l'ottimizzatore JIT si immergerebbe così profondamente nel tuo codice?