Considere los siguientes dos fragmentos de código en una matriz de longitud 2:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
y
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
Supongo que el rendimiento de estas dos piezas debería ser similar después de un calentamiento suficiente.
He comprobado esto utilizando el marco de micro-evaluación comparativa JMH como se describe, por ejemplo, aquí y aquí y observé que el segundo fragmento es más del 10% más rápido.
Pregunta: ¿por qué Java no ha optimizado mi primer fragmento utilizando la técnica básica de desenrollado de bucle?
En particular, me gustaría entender lo siguiente:
- Puedo producir fácilmente un código que es óptimo para los casos de 2 filtros y todavía puede trabajar en caso de otra serie de filtros (imaginar un constructor sencilla):
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters). ¿Puede JITC hacer lo mismo y si no, por qué? - ¿Puede JITC detectar que ' filtros.length == 2 ' es el caso más frecuente y producir el código óptimo para este caso después de un calentamiento? Esto debería ser casi tan óptimo como la versión desenrollada manualmente.
- ¿Puede JITC detectar que una instancia en particular se usa con mucha frecuencia y luego generar un código para esta instancia específica (para la cual sabe que el número de filtros es siempre 2)?
Actualización: obtuve una respuesta de que JITC funciona solo a nivel de clase. Ok lo tengo.
Idealmente, me gustaría recibir una respuesta de alguien con un profundo conocimiento de cómo funciona JITC.
Detalles de ejecución de referencia:
- Probado en las últimas versiones de Java 8 OpenJDK y Oracle HotSpot, los resultados son similares
- Indicadores Java utilizados: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (obtuve resultados similares sin los indicadores elegantes también)
- Por cierto, obtengo una relación de tiempo de ejecución similar si simplemente la ejecuto varios miles de millones de veces en un bucle (no a través de JMH), es decir, el segundo fragmento siempre es claramente más rápido
Salida de referencia típica:
Punto de referencia (filterIndex) Modo Cnt Puntuación Error Unidades
LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44.202 ± 0.224 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38.347 ± 0.063 ns / op
(La primera línea corresponde al primer fragmento, la segunda línea - a la segunda.
Código de referencia 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): no estoy seguro de que ayude (ver el javadoc).
final, pero JIT no ve que todas las instancias de la clase obtendrán una matriz de longitud 2. Para ver eso, tendría que sumergirse en el createLeafFilters()método y analice el código lo suficientemente profundo como para saber que la matriz siempre tendrá 2 longitudes. ¿Por qué crees que el optimizador JIT se sumergiría tanto en tu código?