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?