Улучшено для производительности петли хуже, чем традиционный индексированный поиск?

Я просто натолкнулся на этот, казалось бы, безобидный комментарий, сравнив ArrayList с массивным массивом String. Это от пары лет назад, но OP пишет

Я заметил, что использование для String s: stringsList было примерно на 50% медленнее, чем использование старого цикла for-loop для доступа к списку. Перейти фигурой...

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

Я собираюсь провести бенчмаркинг и опубликовать свои результаты здесь, но это, очевидно, очень беспокоит меня. Я мог бы найти малоинформационную онлайн-информацию об относительной производительности, за исключением того, что пара изредка упоминает, что усиленные циклы для ArrayLists работают намного медленнее в Android.

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

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

TL; DR: усиленные циклы действительно медленнее, чем традиционная петля на основе индексов над arraylist; но для большинства приложений разница должна быть незначительной.

Ответ 1

Проблема заключается в том, что использование Iterator будет медленнее, чем использование прямого поиска. На моей машине разница составляет около 0,13 нс на итерацию. Использование массива вместо этого экономит около 0,15 нс на итерацию. Это должно быть тривиальным в 99% случаев.

public static void main(String... args) {
    int testLength = 100 * 1000 * 1000;
    String[] stringArray = new String[testLength];
    Arrays.fill(stringArray, "a");
    List<String> stringList = new ArrayList<String>(Arrays.asList(stringArray));
    {
        long start = System.nanoTime();
        long total = 0;
        for (String str : stringArray) {
            total += str.length();
        }
        System.out.printf("The for each Array loop time was %.2f ns total=%d%n", (double) (System.nanoTime() - start) / testLength, total);
    }
    {
        long start = System.nanoTime();
        long total = 0;
        for (int i = 0, stringListSize = stringList.size(); i < stringListSize; i++) {
            String str = stringList.get(i);
            total += str.length();
        }
        System.out.printf("The for/get List loop time was %.2f ns total=%d%n", (double) (System.nanoTime() - start) / testLength, total);
    }
    {
        long start = System.nanoTime();
        long total = 0;
        for (String str : stringList) {
            total += str.length();
        }
        System.out.printf("The for each List loop time was %.2f ns total=%d%n", (double) (System.nanoTime() - start) / testLength, total);
    }
}

При запуске с одним миллиардом записей записи печатаются (с использованием обновления для Java 6 26).

The for each Array loop time was 0.76 ns total=1000000000
The for/get List loop time was 0.91 ns total=1000000000
The for each List loop time was 1.04 ns total=1000000000

При запуске с одним миллиардом записей записи печатаются (с использованием OpenJDK 7.)

The for each Array loop time was 0.76 ns total=1000000000
The for/get List loop time was 0.91 ns total=1000000000
The for each List loop time was 1.04 ns total=1000000000

то есть. точно так же.;)

Ответ 2

Каждый утверждает, что X медленнее Y на JVM, который не затрагивает все проблемы, представленные в этой статье ant it second < часть href= "http://www.ibm.com/developerworks/library/j-benchmark2/index.html" rel= "noreferrer" > распространяется на страхи и ложь о производительности типичной JVM. Это относится к комментарию, на который ссылается исходный вопрос, а также ответ GravityBringer. Мне жаль, что я так груб, но если вы не используете подходящую технологию микро-бенчмаркинга, ваши тесты производят действительно искаженные случайные числа.

Расскажите мне, если вас интересуют больше объяснений. Хотя это все в статьях, о которых я говорил.

Ответ 3

Значение GravityBringer не кажется правильным, потому что я знаю, что ArrayList.get() работает так же быстро, как доступ к необработанному массиву после оптимизации VM.

Я дважды запускал тест GravityBringer на моем компьютере, -server mode

50574847
43872295
30494292
30787885
(2nd round)
33865894
32939945
33362063
33165376

Узким местом в таких тестах является чтение/запись памяти. Судя по номерам, все 2 массива находятся в моем кэше L2. Если мы уменьшим размер, чтобы соответствовать кешу L1, или если мы увеличим размер за пределами кеша L2, мы увидим разницу в пропускной способности 10X.

Итератор ArrayList использует один счетчик int. Даже если VM не помещает его в регистр (тело цикла слишком сложное), по крайней мере, оно будет в кеше L1, поэтому r/w of в основном свободны.

Конечным ответом, конечно, является проверка вашей конкретной программы в вашей конкретной среде.

Хотя это не полезно играть агностиком всякий раз, когда возникает вопрос о контроле.

Ответ 4

Ситуация ухудшилась для ArrayLists. На моем компьютере, работающем на Java 6.26, есть разница в четыре раза. Интересно (и, возможно, вполне логично), нет никакой разницы для сырых массивов. Я проверил следующий тест:

    int testSize = 5000000;

    ArrayList<Double> list = new ArrayList<Double>();
    Double[] arr = new Double[testSize];

    //set up the data - make sure data doesn't have patterns
    //or anything compiler could somehow optimize
    for (int i=0;i<testSize; i++)
    {
        double someNumber = Math.random();
        list.add(someNumber);
        arr[i] = someNumber;
    }

    //ArrayList foreach
    long time = System.nanoTime();
    double total1 = 0;
    for (Double k: list)
    {
        total1 += k;
    }
    System.out.println (System.nanoTime()-time);

    //ArrayList get() method
    time = System.nanoTime();
    double total2 = 0;
    for (int i=0;i<testSize;i++)
    {
        total2 += list.get(i);  
    }
    System.out.println (System.nanoTime()-time);        

    //array foreach
    time = System.nanoTime();
    double total3 = 0;
    for (Double k: arr)
    {
        total3 += k;
    }
    System.out.println (System.nanoTime()-time);

    //array indexing
    time = System.nanoTime();
    double total4 = 0;
    for (int i=0;i<testSize;i++)
    {
        total4 += arr[i];
    }
    System.out.println (System.nanoTime()-time);

    //would be strange if different values were produced,
    //but no, all these are the same, of course
    System.out.println (total1);
    System.out.println (total2);        
    System.out.println (total3);
    System.out.println (total4);

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

Время работы (в наносекундах):

ArrayList foreach: 248,351,782

ArrayList get(): 60 657 907

массив foreach: 27,381,576

Прямая индексация массива: 27 468 091