Какова стоимость вызова array.length

При обновлении циклов for до циклов for-each в нашем приложении я наткнулся на множество таких «шаблонов»:

for (int i = 0, n = a.length; i < n; i++) {
    ...
}

вместо

for (int i = 0; i < a.length; i++) {
    ...
}

Я вижу, что вы повышаете производительность для коллекций, потому что вам не нужно вызывать метод size() в каждом цикле. А с массивами??

Вот и возник вопрос: array.length дороже ли обычной переменной?


person Stroboskop    schedule 30.07.2009    source источник
comment
для удобочитаемости - используйте второй шаблон или цикл for..each. Пожалуйста!   -  person Andreas Dolk    schedule 30.07.2009


Ответы (8)


Нет, вызов array.length является O(1) или операцией с постоянным временем.

Поскольку .length является (действует как) public final членом array, доступ к нему не медленнее, чем к локальной переменной. (Это сильно отличается от вызова такого метода, как size())

Современный JIT-компилятор, скорее всего, сразу же оптимизирует вызов .length.

Вы можете убедиться в этом, просмотрев исходный код JIT-компилятора в OpenJDK или заставив JVM выгрузить скомпилированный JIT собственный код и изучить код.

Обратите внимание, что могут быть случаи, когда компилятор JIT не может этого сделать; например

  1. если вы отлаживаете метод включения или
  2. если в теле цикла достаточно локальных переменных для принудительного сброса регистров.
person jjnguy    schedule 30.07.2009
comment
@jjnguy: Разве вызов array.length не может быть O (1) и по-прежнему занимает больше времени, чем int len = array.length; i < len;, который также будет O (1)? - person Grant Wagner; 30.07.2009
comment
Ну, если вы беспокоитесь о таких вещах, вам следует программировать на C или C++ imho. (Но, да, вы можете быть правы) - person jjnguy; 30.07.2009
comment
@jjnguy: Просто быть педантичным и указать, что все O (1) не созданы равными. - person Grant Wagner; 30.07.2009
comment
Я добавил больше информации. Теперь должно быть больше смысла. - person jjnguy; 30.07.2009
comment
Если разница во времени между поиском значения в прямом вызове переменной .length и n так велика, то в коде может быть что-то еще не так. Профилировщик поможет определить горячие точки. Мне это больше пахнет оптимизацией без представления, и я полностью согласен с jjnguy. - person aperkins; 30.07.2009
comment
@aperkins, рад, что кто-то на моей стороне! - person jjnguy; 30.07.2009
comment
Современный компилятор, скорее всего, сразу же оптимизирует вызов .length. [нужна цитата] - person xehpuk; 28.04.2015
comment
@xehpuk - исходный код всех JIT-компиляторов OpenJDK, начиная с Java 6, находится в свободном доступе для изучения :-) - person Stephen C; 15.02.2019
comment
@StephenC Я не был тем, кто утверждал это. Вы приглашены, чтобы доказать это. В прошлый раз, когда я проверял, по крайней мере javac ничего не оптимизировалось. Я мало знаю о JIT-компиляторах. - person xehpuk; 15.02.2019
comment
@xehpuk - я знаю. Вы были тем, кто оспаривал это. Я дал вам цитату, которую вы просили. Чтобы вы могли больше узнать о JIT-компиляторах :-) - person Stephen C; 15.02.2019

У меня было немного времени за обедом:

public static void main(String[] args) {
    final int[] a = new int[250000000];
    long t;

    for (int j = 0; j < 10; j++) {
        t = System.currentTimeMillis();
        for (int i = 0, n = a.length; i < n; i++) { int x = a[i]; }
        System.out.println("n = a.length: " + (System.currentTimeMillis() - t));

        t = System.currentTimeMillis();
        for (int i = 0; i < a.length; i++) { int x = a[i]; }
        System.out.println("i < a.length: " + (System.currentTimeMillis() - t));
    }
}

Результаты:

n = a.length: 672
i < a.length: 516
n = a.length: 640
i < a.length: 516
n = a.length: 656
i < a.length: 516
n = a.length: 656
i < a.length: 516
n = a.length: 640
i < a.length: 532
n = a.length: 640
i < a.length: 531
n = a.length: 641
i < a.length: 516
n = a.length: 656
i < a.length: 531
n = a.length: 656
i < a.length: 516
n = a.length: 656
i < a.length: 516

Примечания:

  1. Если вы измените тесты, то n = a.length покажет, что он быстрее, чем i < a.length, примерно наполовину, вероятно, из-за сборки мусора (?).
  2. Я не мог увеличить 250000000, потому что получил OutOfMemoryError в 270000000.

Дело в том, что это то, что делают все остальные, вам нужно запускать Java из памяти, и вы все еще не видите существенной разницы в скорости между двумя альтернативами. Потратьте время разработки на то, что действительно важно.

person Grant Wagner    schedule 30.07.2009
comment
@wbkang: Определение того, что размер кучи по умолчанию недостаточен для очень большого массива, на самом деле не было целью упражнения. - person Grant Wagner; 31.07.2009
comment
@Brian: Всегда ли это имеет значение в C/C++? Если бы у вас был цикл из 1000 итераций, который выполнял вычисления за 1 секунду на каждой итерации, были бы вы действительно обеспокоены тем, что доступ к длине массива занимает на несколько миллисекунд больше времени в одну сторону по сравнению с другой? Ваше время было бы гораздо лучше потрачено на оптимизацию расчета, который занимает 1 секунду 1000 раз. Я понимаю, что вы хотели сказать, что иногда это имеет значение, но даже для C/C++ это не всегда имеет значение. Как я уже сказал, тратьте время разработки на то, что действительно важно. - person Grant Wagner; 31.07.2009
comment
Я немного разочарован тем, что компилятор не понял, что весь цикл не имеет абсолютно никакого эффекта, и просто оптимизировал его. - person 5gon12eder; 27.10.2015
comment
Я называю 150 мс существенной разницей, если вы работаете с игровыми тиками в 50 мс :-), но опять же, вы никогда не будете использовать такие огромные массивы. - person Tschallacka; 26.02.2018
comment
Это плохой бенчмарк, и результаты не показывают, что произойдет в реальном коде. Когда я запускаю его с Java 11, похоже, что циклы оптимизируются JIT-компилятором. У меня получилось 40,5,15,0,0,0,0,0,0,0,0,0,0,0,0,0... Судя по цифрам, которые вы получали, я бы сказал, что метод не компилируется JIT. - person Stephen C; 15.02.2019

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

person IRBMe    schedule 30.07.2009
comment
+1. Если вы не делаете что-то невероятно тривиальное внутри цикла, все, что вы делаете внутри цикла, вероятно, займет больше времени, чем доступ к .length на несколько порядков. - person Grant Wagner; 30.07.2009

Длина массива хранится как переменная-член массива (не то же самое, что элемент) в Java, поэтому выборка этой длины является операцией с постоянным временем, такой же, как чтение переменной-члена из обычного класса. Многие старые языки, такие как C и C++, не сохраняют длину как часть массива, поэтому вам нужно сохранить длину до начала цикла. Вам не нужно делать это в Java.

person Bill the Lizard    schedule 30.07.2009
comment
‹придирка› Это не совсем переменная-член; на самом деле он имеет свой собственный специальный байт-код. ‹/придирка› - person Michael Myers; 30.07.2009

В этом случае, почему бы вам не сделать обратный цикл:

for (int i = a.length - 1; i >= 0; --i) {
    ...
}

Здесь есть 2 микрооптимизации:

  • Разворот петли
  • Постфиксный декремент
person instcode    schedule 31.07.2009
comment
почему обращение петли лучше? Потому что сравнение с 0 быстрее? - person Stroboskop; 03.08.2009
comment
Да, я так думаю. Кто-то сделал эталон для этого: mkyong .com/java/ - person instcode; 03.08.2009
comment
А почему бы и нет? Потому что я хочу, чтобы код был простым. Причина, по которой я задал этот вопрос, заключалась в том, чтобы просто посмотреть, есть ли разумная причина использовать шаблон, описанный в вопросе. - person Stroboskop; 07.08.2009
comment
@instcode Декремент постфикса быстрее, чем инкремент постфикса? - person Abdul; 29.07.2015

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

На уровне байт-кода получение длины массива выполняется с помощью байт-кода arraylength. Я не знаю, медленнее ли это, чем байт-код iload, но разницы не должно быть достаточно, чтобы заметить.

person Michael Myers    schedule 30.07.2009

Этот ответ с точки зрения C #, но я думаю, что то же самое относится и к Java.

В C# идиома

for (int i = 0; i < a.length; i++) {    ...}

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

Это может быть или не быть распознано с помощью кода, такого как:

for (int i = 0, n = a.length; i < n; i++) {    ...}

or

n = a.length;

for (int i = 0; i < n; i++) {   ...}

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

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

person Michael Burr    schedule 30.07.2009
comment
Да, я думаю, что Java делает ту же оптимизацию (по крайней мере, более поздние JVM должны). - person Michael Myers; 30.07.2009

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

person Chris Vest    schedule 30.07.2009