Рассмотрим следующие два фрагмента кода для массива длиной 2:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
а также
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
Я бы предположил, что производительность этих двух частей должна быть одинаковой после достаточного прогрева.
Я проверил это с помощью JMH, как описано здесь и здесь, и заметил, что второй фрагмент работает более чем на 10% быстрее.
Вопрос: почему Java не оптимизировал мой первый фрагмент, используя базовую технику развертывания цикла?
В частности, я хотел бы понять следующее:
- Я могу легко создать код , который является оптимальным для случаев 2 фильтров и по- прежнему может работать в случае другого количества фильтров (представьте себе простой строитель)
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)
. Может ли JITC сделать то же самое, и если нет, то почему? - Может ли JITC обнаружить, что « filters.length == 2 » является наиболее частым случаем, и выдать код, который является оптимальным для этого случая после некоторого прогрева? Это должно быть почти так же оптимально, как и версия, развернутая вручную.
- Может ли JITC обнаружить, что конкретный экземпляр используется очень часто, и затем создать код для этого конкретного экземпляра (для которого он знает, что количество фильтров всегда равно 2)?
Обновление: получил ответ, что JITC работает только на уровне класса. Хорошо понял.
В идеале я хотел бы получить ответ от кого-то с глубоким пониманием того, как работает JITC.
Детали запуска теста:
- Пробовал на последних версиях Java 8 OpenJDK и Oracle HotSpot, результаты похожи
- Используемые флаги Java: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (также были получены аналогичные результаты без необычных флагов)
- Кстати, я получаю аналогичное соотношение времени выполнения, если просто запустить его несколько миллиардов раз в цикле (не через JMH), то есть второй фрагмент всегда явно быстрее
Типичный результат теста:
Бенчмарк (filterIndex) Режим Cnt Оценка Ошибка Единицы измерения
LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44.202 ± 0.224 нс /
опт
(Первая строка соответствует первому фрагменту, вторая строка - второй.
Полный код теста:
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)
Не уверен, что это помогает (см. Javadoc).
final
, но JIT не видит, что все экземпляры класса получат массив длины 2. Чтобы увидеть это, ему придется погрузиться в createLeafFilters()
метод и анализировать код достаточно глубоко, чтобы понять, что массив всегда будет 2 длинным. Как вы думаете, почему оптимизатор JIT так глубоко погрузится в ваш код?