Статьи

Локальные переменные внутри цикла и производительность

обзор

Иногда возникает вопрос о том, сколько работы занимает выделение новой локальной переменной. Мне всегда казалось, что код оптимизируется до такой степени, что эта стоимость является статической, то есть выполняется один раз, а не каждый раз, когда код выполняется.

Недавно Ишвор Гурунг предложил рассмотреть возможность перемещения некоторых локальных переменных за пределы цикла. Я подозревал, что это не будет иметь значения, но я никогда не проверял, было ли это так.

Тест

Это тест, который я провел:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String... args) {
    for (int i = 0; i < 10; i++) {
        testInsideLoop();
        testOutsideLoop();
    }
}
 
private static void testInsideLoop() {
    long start = System.nanoTime();
    int[] counters = new int[144];
    int runs = 200 * 1000;
    for (int i = 0; i < runs; i++) {
        int x = i % 12;
        int y = i / 12 % 12;
        int times = x * y;
        counters[times]++;
    }
    long time = System.nanoTime() - start;
    System.out.printf("Inside: Average loop time %.1f ns%n", (double) time / runs);
}
 
private static void testOutsideLoop() {
    long start = System.nanoTime();
    int[] counters = new int[144];
    int runs = 200 * 1000, x, y, times;
    for (int i = 0; i < runs; i++) {
        x = i % 12;
        y = i / 12 % 12;
        times = x * y;
        counters[times]++;
    }
    long time = System.nanoTime() - start;
    System.out.printf("Outside: Average loop time %.1f ns%n", (double) time / runs);
}

и вывод закончился:

Внутри : среднее время цикла 3,6 нс
Снаружи : среднее время цикла 3,6 нс
Внутри : среднее время цикла 3,6 нс
Снаружи : среднее время цикла 3,6 нс

Увеличение времени выполнения теста до 100 миллионов итераций практически не повлияло на результаты.

Внутри : Среднее время цикла 3,8 нс
Снаружи : среднее время цикла 3,8 нс
Внутри : Среднее время цикла 3,8 нс
Снаружи : среднее время цикла 3,8 нс

Заменив модуль и умножение на >>, &, + I получил

1
2
3
int x = i & 15;
int y = (i >> 4) & 15;
int times = x + y;

печать

Внутри : Среднее время цикла 1,2 нс
Снаружи : среднее время цикла 1,2 нс
Внутри : Среднее время цикла 1,2 нс
Снаружи : среднее время цикла 1,2 нс

Хотя модуль является относительно дорогим, разрешение теста составляет 0,1 нс или менее 1/3 тактового цикла. Это покажет любую разницу между двумя тестами с точностью до этого.

Используя суппорт

Как комментирует @maaartinus, Caliper — это библиотека для микро-бенчмаркинга, поэтому меня интересовало, насколько медленнее будет делать код вручную.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String... args) {
    Runner.main(LoopBenchmark.class, args);
}
 
public static class LoopBenchmark extends SimpleBenchmark {
    public void timeInsideLoop(int reps) {
        int[] counters = new int[144];
        for (int i = 0; i < reps; i++) {
            int x = i % 12;
            int y = i / 12 % 12;
            int times = x * y;
            counters[times]++;
        }
    }
 
    public void timeOutsideLoop(int reps) {
        int[] counters = new int[144];
        int x, y, times;
        for (int i = 0; i < reps; i++) {
            x = i % 12;
            y = i / 12 % 12;
            times = x * y;
            counters[times]++;
        }
    }
}

Первое, что нужно отметить, это то, что код короче, поскольку он не включает синхронизацию и печать кода котельной пластины. Выполнение этого я получаю на той же машине, что и первый тест.

1
2
3
4
5
6
7
8
9
0% Scenario{vm=java, trial=0, benchmark=InsideLoop} 4.23 ns; σ=0.01 ns @ 3 trials
50% Scenario{vm=java, trial=0, benchmark=OutsideLoop} 4.23 ns; σ=0.01 ns @ 3 trials
 
benchmark   ns linear runtime
InsideLoop 4.23 ==============================
OutsideLoop 4.23 =============================
 
vm: java
trial: 0

Замена модуля со смещением и и

1
2
3
4
5
6
7
8
9
0% Scenario{vm=java, trial=0, benchmark=InsideLoop} 1.27 ns; σ=0.01 ns @ 3 trials
50% Scenario{vm=java, trial=0, benchmark=OutsideLoop} 1.27 ns; σ=0.00 ns @ 3 trials
 
benchmark   ns linear runtime
InsideLoop 1.27 =============================
OutsideLoop 1.27 ==============================
 
vm: java
trial: 0

Это согласуется с первым результатом и только на 0,4 — 0,6 нс медленнее для одного теста. (около двух тактов), и рядом нет разницы для смены, и, плюс тест. Это может быть связано с тем, что штангенциркуль выбирает данные, но не меняет результат.

Ничего не стоит, что при запуске реальных программ вы обычно получаете больше времени, чем микропроцессор, поскольку программа будет выполнять больше задач, поэтому кэширование и предсказания ветвлений не столь идеальны. Небольшая завышенная оценка времени может быть ближе к тому, что вы можете ожидать увидеть в реальной программе.

Вывод

Это указывало на то, что в данном случае это не имело никакого значения. Я все еще подозреваю, что стоимость выделения локальных переменных не однажды, когда код компилируется JIT, и нет необходимости учитывать стоимость каждой итерации.

Справка: можно ли оптимизировать синхронизацию? от нашего партнера JCG Питера Лоури из блога Vanilla Java .