Подтвердить что ты не робот

Использует ли промежуточную переменную вместо array.length ускорить цикл for-loop?

"Рекомендации по производительности" в документации по Android имеет довольно смелое утверждение:

one() работает быстрее. Он выводит все на локальные переменные, избегая поиска. Только длина массива дает преимущество в производительности.

где он ссылается на этот фрагмент кода:

int len = localArray.length;

for (int i = 0; i < len; ++i) {
    sum += localArray[i].mSplat;
}

Это меня очень удивило, потому что localArray.length просто обращается к целому числу, и если вы будете использовать промежуточную переменную, вам придется сделать то же самое еще раз. Действительно ли мы говорим, что промежуточная переменная, которая должна перейти только в x вместо y.x, быстрее?

Я рассмотрел этот вопрос, который касается той же идеи, но вместо этого использует arraylist и его последующий метод .size(). Здесь консенсус, казалось, заключался в том, что не будет никакой разницы, так как этот вызов метода, вероятно, просто будет привязан к целочисленному доступу в любом случае (это именно тот сценарий, который у нас есть здесь).

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

Учитывая следующий исходный код:

public void MethodOne() {
    int[] arr = new int[5];
    for (int i = 0; i < arr.length; i++) { }
}

public void MethodTwo() {
    int[] arr = new int[5];
    int len = arr.length;
    for (int i = 0; i < len; i++) { }
}

Я получаю следующий байт-код:

public void MethodOne();
    Code:
        0: iconst_5
        1: newarray       int
        3: astore_1
        4: iconst_0
        5: istore_2
        6: iload_2
        7: aload_1
        8: arraylength
        9: if_icmpge     18
        12: iinc          2, 1
        15: goto          6
        18: return

public void MethodTwo();
    Code:
        0: iconst_5
        1: newarray       int
        3: astore_1
        4: aload_1
        5: arraylength
        6: istore_2
        7: iconst_0
        8: istore_3
        9: iload_3
        10: iload_2
        11: if_icmpge     20
        14: iinc          3, 1
        17: goto          9
        20: return

Они отличаются в следующих инструкциях:

Метод один

6: iload_2
7: aload_1
8: arraylength
9: if_icmpge     18
12: iinc          2, 1
15: goto          6
18: return

Метод два

9: iload_3
10: iload_2
11: if_icmpge     20
14: iinc          3, 1
17: goto          9
20: return

Теперь я не уверен на 100%, как мне нужно интерпретировать 8: arraylength, но я думаю, что это просто указывает поле, к которому вы обращаетесь. Первый метод загружает индексный счетчик и массив и обращается к полю arraylength, а второй метод загружает индексный счетчик и промежуточную переменную.

Я сравнивал два метода с JMH (10 разминок, 10 итераций, 5 вилок), что дает мне следующий результат сравнения:

c.m.m.Start.MethodOne    thrpt        50  3447184.351    19973.900   ops/ms
c.m.m.Start.MethodTwo    thrpt        50  3435112.281    32639.755   ops/ms

который говорит мне, что разница пренебрежимо мала.


В чем заключается заявка на документацию Android о том, что с использованием промежуточной переменной в состоянии цикла на основе?

4b9b3361

Ответ 1

Вы неправильно поняли документацию. Они не относятся к тому, что вы описали (хотя я не обвиняю вас, они должны приложить больше усилий в эти документы:)).

Он выводит все на локальные переменные, избегая поиска.

Избегая поиска, они ссылаются на поле и стоимость доступа к локальной переменной. Для доступа к полю (mArray в примере в документах) сначала необходимо загрузить this, а затем загрузить поле на основе фиксированного смещения от this.

Через некоторое время JIT, вероятно, выяснит, что происходит и оптимизирует доступ к полю (если это поле не volatile или какая-то другая форма синхронизации происходит в цикле) и переписать код так, чтобы все переменные, участвующие в цикле, получают доступ/изменяются в регистры процессора и кэша до конца цикла.

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

for (int i = 0; i < array.length; ++i) {
    process(array[i]);
}

Если array - это поле, а process вызывает тысячи строк сложного кода, то JIT может с трудом проверить, было ли поле array изменено где-то в цикле, чтобы ссылаться на некоторый другой массив, который имеет разной длины.

Очевидно, гораздо проще проверить, изменяется ли локальная переменная в этом случае (три строки кода).

Ответ 2

Фактически нет, это не ускорило цикл, ваша идея правильная при использовании String.length()
разница в том, что array.length - это просто поле, которое имеет значение, которое вы просто используете непосредственно.
String.length() - это метод, который требует времени для выполнения.