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?