Considere os dois trechos de código a seguir em uma matriz de comprimento 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);
}
Eu assumiria que o desempenho dessas duas peças deveria ser semelhante após aquecimento suficiente.
Eu verifiquei isso usando a estrutura de micro-benchmarking JMH conforme descrito, por exemplo, aqui e aqui e observei que o segundo trecho é 10% mais rápido.
Pergunta: por que o Java não otimizou meu primeiro snippet usando a técnica básica de desenrolamento de loop?
Em particular, gostaria de entender o seguinte:
- I pode facilmente produzir um código que é ideal para casos de 2 filtros e ainda pode trabalhar no caso de um outro número de filtros (imagine um construtor simples):
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)
. O JITC pode fazer o mesmo e, se não, por quê? - O JITC pode detectar que ' filters.length == 2 ' é o caso mais frequente e produzir o código ideal para esse caso após algum aquecimento? Isso deve ser quase tão ideal quanto a versão desenrolada manualmente.
- O JITC pode detectar que uma instância específica é usada com muita frequência e produzir um código para essa instância específica (para a qual sabe que o número de filtros é sempre 2)?
Atualização: obteve uma resposta de que o JITC funciona apenas em nível de classe. OK, entendi.
Idealmente, gostaria de receber uma resposta de alguém com um profundo entendimento de como o JITC funciona.
Detalhes de execução de referência:
- Tentado nas versões mais recentes do Java 8 OpenJDK e Oracle HotSpot, os resultados são semelhantes
- Sinalizadores Java usados: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (obteve resultados semelhantes sem os sinalizadores extravagantes)
- A propósito, obtenho uma taxa de tempo de execução semelhante se eu a executar vários bilhões de vezes em um loop (não via JMH), ou seja, o segundo trecho é sempre claramente mais rápido
Saída típica de benchmark:
Modo de referência (filterIndex) Modo Cnt Score Error Units
LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44.202 ± 0.224 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38.347 ± 0.063 ns / op
(A primeira linha corresponde ao primeiro trecho, a segunda linha - à segunda.
Código de referência 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)
: não tenho certeza de que ajuda (consulte o javadoc).
final
, mas o JIT não vê que todas as instâncias da classe receberão uma matriz de comprimento 2. Para ver isso, teria que mergulhar no createLeafFilters()
e analise o código com profundidade suficiente para saber que a matriz sempre terá 2 anos. Por que você acredita que o otimizador JIT entraria tão profundamente em seu código?