Розглянемо наступні два фрагменти коду на масиві довжиною 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 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38,347 ± 0,063 ns / op
(Перший рядок відповідає першому фрагменту, другий рядок - другому.
Повний код еталону:
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 зануриться так глибоко у ваш код?