Considérez les deux extraits de code suivants sur un tableau de longueur 2:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
et
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
Je suppose que les performances de ces deux pièces devraient être similaires après un échauffement suffisant.
J'ai vérifié cela en utilisant le cadre de micro-analyse comparative JMH comme décrit par exemple ici et ici et j'ai observé que le deuxième extrait est plus de 10% plus rapide.
Question: pourquoi Java n'a-t-il pas optimisé mon premier extrait en utilisant la technique de déroulement de boucle de base?
En particulier, j'aimerais comprendre ce qui suit:
- Je peux facilement produire un code qui est optimal pour les cas de 2 filtres et peut encore fonctionner en cas d'un autre nombre de filtres (imaginez un constructeur simple requise ):
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)
. Le JITC peut-il faire de même et sinon, pourquoi? - Le JITC peut-il détecter que ' filters.length == 2 ' est le cas le plus fréquent et produire le code optimal pour ce cas après un certain échauffement? Cela devrait être presque aussi optimal que la version déroulée manuellement.
- JITC peut-il détecter qu'une instance particulière est utilisée très fréquemment, puis produire un code pour cette instance spécifique (pour lequel il sait que le nombre de filtres est toujours 2)?
Mise à jour: obtenu une réponse que JITC ne fonctionne qu'au niveau de la classe. OK, j'ai compris.
Idéalement, j'aimerais recevoir une réponse d'une personne ayant une compréhension approfondie du fonctionnement du JITC.
Détails de l'analyse comparative:
- Testé sur les dernières versions de Java 8 OpenJDK et Oracle HotSpot, les résultats sont similaires
- Indicateurs Java utilisés: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (a obtenu des résultats similaires sans les indicateurs fantaisie également)
- Soit dit en passant, j'obtiens un rapport de durée d'exécution similaire si je l'exécute simplement plusieurs milliards de fois dans une boucle (pas via JMH), c'est-à-dire que le deuxième extrait est toujours nettement plus rapide
Sortie de référence typique:
Benchmark (filterIndex) Mode Cnt Score Erreur Unités
LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44,202 ± 0,224 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38,347 ± 0,063 ns / op
(La première ligne correspond au premier extrait, la deuxième ligne - à la seconde.
Code de référence complet:
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)
: pas sûr que ça aide (voir le javadoc).
final
, mais JIT ne voit pas que toutes les instances de la classe obtiendront un tableau de longueur 2. Pour voir cela, il faudrait plonger dans le createLeafFilters()
et analysez le code suffisamment profondément pour apprendre que le tableau sera toujours long. Pourquoi pensez-vous que l'optimiseur JIT plongerait si profondément dans votre code?