Массив или Список в Java. Что быстрее? - PullRequest
329 голосов
/ 04 апреля 2009

Мне нужно хранить тысячи строк в памяти для последовательного доступа на Java. Должен ли я хранить их в массиве или использовать какой-то список?

Поскольку массивы хранят все данные в непрерывном фрагменте памяти (в отличие от списков), вызовет ли проблема использование массива для хранения тысяч строк?

Ответы [ 31 ]

344 голосов
/ 04 апреля 2009

Я предлагаю вам использовать профилировщик для тестирования, который работает быстрее.

Мое личное мнение, что вы должны использовать списки.

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

158 голосов
/ 04 апреля 2009

С точки зрения Java вы должны учитывать, какие данные абстракция наиболее соответствуют вашим потребностям. Помните, что в Java список - это абстрактный, а не конкретный тип данных. Вы должны объявить строки как List, а затем инициализировать их, используя реализацию ArrayList.

List<String> strings = new ArrayList<String>();

Такое разделение абстрактного типа данных и конкретной реализации является одним из ключевых аспектов объектно-ориентированного программирования.

ArrayList реализует абстрактный тип данных List, используя массив в качестве базовой реализации. Скорость доступа практически идентична массиву, с дополнительными преимуществами возможности добавлять и вычитать элементы в список (хотя это операция O (n) с ArrayList) и это, если вы решите изменить базовую реализацию позже вы можете. Например, если вы понимаете, что вам нужен синхронизированный доступ, вы можете изменить реализацию на Vector, не переписывая весь свой код.

Фактически, ArrayList был специально разработан для замены низкоуровневой конструкции массива в большинстве контекстов. Если бы Java разрабатывался сегодня, вполне возможно, что массивы были бы полностью исключены в пользу конструкции ArrayList.

Поскольку массивы хранят все данные в непрерывном фрагменте памяти (в отличие от списков), вызовет ли проблема использование массива для хранения тысяч строк?

В Java все коллекции хранят только ссылки на объекты, а не сами объекты. Оба массива и ArrayList будут хранить несколько тысяч ссылок в непрерывном массиве, поэтому они практически идентичны. Можно предположить, что непрерывный блок из нескольких тысяч 32-битных ссылок всегда будет легко доступен на современном оборудовании. Это, конечно, не гарантирует, что вам не хватит памяти вообще, просто то, что непрерывный блок памяти требует несложных действий.

92 голосов
/ 04 апреля 2009

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

Но, как всегда, при оптимизации вы должны всегда выполнять следующие шаги:

  • Не оптимизируйте, пока у вас не будет хорошей, чистой и рабочей версии вашего кода. Переход на универсальные типы уже вполне может быть мотивирован на этом этапе.
  • Если у вас есть хорошая и чистая версия, решите, достаточно ли она быстра.
  • Если он недостаточно быстр, измерить его производительность . Этот шаг важен по двум причинам. Если вы не будете измерять, вы не будете (1) узнавать о последствиях ваших оптимизаций и (2) знать, где оптимизировать.
  • Оптимизируйте самую горячую часть вашего кода.
  • Измерьте снова. Это так же важно, как и измерение ранее. Если оптимизация не улучшила ситуацию, верните ее . Помните, код без Оптимизация была чистой, красивой и работающей.
86 голосов
/ 15 мая 2013

Хотя ответы, предлагающие использовать ArrayList, имеют смысл в большинстве сценариев, на настоящий вопрос об относительной производительности ответа пока нет.

Есть несколько вещей, которые вы можете сделать с массивом:

  • создать его
  • установить предмет
  • получить предмет
  • клонировать / скопировать

Общий вывод

Хотя операции получения и установки на ArrayList выполняются несколько медленнее (соответственно, 1 и 3 наносекунды на вызов на моей машине), очень мало затрат на использование ArrayList по сравнению с массивом для любого не интенсивного использования. Однако следует помнить несколько вещей:

  • операции по изменению размера списка (при вызове list.add(...)) являются дорогостоящими, и при возможности следует попытаться установить начальную емкость на адекватном уровне (обратите внимание, что такая же проблема возникает при использовании массива)
  • при работе с примитивами массивы могут быть значительно быстрее, поскольку они позволяют избежать многих преобразований в бокс / распаковку
  • приложение, которое получает / устанавливает значения только в ArrayList (не очень часто!), Может получить прирост производительности более чем на 25% при переключении на массив

Подробные результаты

Вот результаты, которые я измерил для этих трех операций, используя jmh библиотеку бенчмаркинга (раз в наносекундах) с JDK 7 на стандартном настольном компьютере x86. Обратите внимание, что ArrayList никогда не изменяется в тестах, чтобы убедиться, что результаты сопоставимы. Код эталонного теста доступен здесь .

Создание массива / ArrayList

Я выполнил 4 теста, выполнив следующие инструкции:

  • createArray1: Integer[] array = new Integer[1];
  • createList1: List<Integer> list = new ArrayList<> (1);
  • createArray10000: Integer[] array = new Integer[10000];
  • createList10000: List<Integer> list = new ArrayList<> (10000);

Результаты (в наносекундах на звонок, 95% достоверность):

a.p.g.a.ArrayVsList.CreateArray1         [10.933, 11.097]
a.p.g.a.ArrayVsList.CreateList1          [10.799, 11.046]
a.p.g.a.ArrayVsList.CreateArray10000    [394.899, 404.034]
a.p.g.a.ArrayVsList.CreateList10000     [396.706, 401.266]

Вывод: заметной разницы нет .

получить операции

Я выполнил 2 теста, выполнив следующие инструкции:

  • getList: return list.get(0);
  • getArray: return array[0];

Результаты (в наносекундах на звонок, 95% достоверность):

a.p.g.a.ArrayVsList.getArray   [2.958, 2.984]
a.p.g.a.ArrayVsList.getList    [3.841, 3.874]

Вывод: получение из массива примерно на 25% быстрее , чем из ArrayList, хотя разница составляет всего лишь одну наносекунду.

операции над множествами

Я выполнил 2 теста, выполнив следующие инструкции:

  • setList: list.set(0, value);
  • setArray: array[0] = value;

Результаты (в наносекундах за звонок):

a.p.g.a.ArrayVsList.setArray   [4.201, 4.236]
a.p.g.a.ArrayVsList.setList    [6.783, 6.877]

Вывод: операции над множествами над массивами примерно на 40% быстрее , чем над списками, но, как и при получении, каждая операция над множеством занимает несколько наносекунд - поэтому, чтобы разница достигла 1 секунды, потребуется устанавливать элементы в списке / массиве сотни миллионов раз!

Клон / копия

Конструктор копирования ArrayList делегирует Arrays.copyOf, поэтому производительность идентична копии массива (копирование массива с помощью clone, Arrays.copyOf или System.arrayCopy не оказывает существенного влияния на производительность ). 1111 *

22 голосов
/ 04 апреля 2009

Я предполагаю, что оригинальный постер взят из C ++ / STL фона, что вызывает некоторую путаницу. В C ++ std::list это двусвязный список.

В Java [java.util.]List - это интерфейс без реализации (чистый абстрактный класс в терминах C ++). List может быть двусвязным списком - предоставляется java.util.LinkedList. Тем не менее, в 99 случаях из 100, когда вы хотите сделать новый List, вы хотите использовать вместо него java.util.ArrayList, что является грубым эквивалентом C ++ std::vector. Существуют и другие стандартные реализации, например, возвращаемые java.util.Collections.emptyList() и java.util.Arrays.asList().

С точки зрения производительности очень малое попадание из-за необходимости проходить через интерфейс и дополнительный объект, однако встраивание во время выполнения означает, что это редко имеет какое-либо значение. Также помните, что String обычно является объектом плюс массив. Так что для каждой записи у вас, вероятно, есть два других объекта. В C ++ std::vector<std::string>, хотя копирование по значению без указателя как такового, символьные массивы будут формировать объект для строки (и они обычно не будут общими). ​​

Если этот конкретный код действительно чувствителен к производительности, вы можете создать один массив char[] (или даже byte[]) для всех символов всех строк, а затем массив смещений. IIRC, так реализован javac.

13 голосов
/ 16 сентября 2012

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

Однако, если вы выполняете постоянную, тяжелую итерацию с небольшими структурными изменениями (без добавления и удаления), например, для рендеринга программной графики или пользовательской виртуальной машины, мои сравнительные тесты последовательного доступа показывают, что ArrayLists равны 1,5 x медленнее, чем массивы в моей системе (Java 1.6 на моем годовалом iMac).

Некий код:

import java.util.*;

public class ArrayVsArrayList {
    static public void main( String[] args ) {

        String[] array = new String[300];
        ArrayList<String> list = new ArrayList<String>(300);

        for (int i=0; i<300; ++i) {
            if (Math.random() > 0.5) {
                array[i] = "abc";
            } else {
                array[i] = "xyz";
            }

            list.add( array[i] );
        }

        int iterations = 100000000;
        long start_ms;
        int sum;

        start_ms = System.currentTimeMillis();
        sum = 0;

        for (int i=0; i<iterations; ++i) {
          for (int j=0; j<300; ++j) sum += array[j].length();
        }

        System.out.println( (System.currentTimeMillis() - start_ms) + " ms (array)" );
        // Prints ~13,500 ms on my system

        start_ms = System.currentTimeMillis();
        sum = 0;

        for (int i=0; i<iterations; ++i) {
          for (int j=0; j<300; ++j) sum += list.get(j).length();
        }

        System.out.println( (System.currentTimeMillis() - start_ms) + " ms (ArrayList)" );
        // Prints ~20,800 ms on my system - about 1.5x slower than direct array access
    }
}
11 голосов
/ 04 апреля 2009

Ну, во-первых, стоит уточнить, имеете ли вы в виду «список» в смысле классических структур данных компа (т. Е. Связанный список) или вы имеете в виду java.util.List? Если вы имеете в виду java.util.List, это интерфейс. Если вы хотите использовать массив, просто используйте реализацию ArrayList, и вы получите поведение и семантику, подобные массиву. Проблема решена.

Если вы имеете в виду массив против связанного списка, это немного другой аргумент, для которого мы возвращаемся к Big O (вот простое английское объяснение 1004 *, если это незнакомый термин.

Массив;

  • Произвольный доступ: O (1);
  • Вставка: O (n);
  • Удалить: O (n).

Связанный список:

  • Произвольный доступ: O (n);
  • Вставка: O (1);
  • Удалить: O (1).

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

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

11 голосов
/ 04 апреля 2009

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

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

n.b. Я заметил , что использование for String s: stringsList было примерно на 50% медленнее, чем использование цикла for в старом стиле для доступа к списку. Пойди разберись ... Вот две функции, которые я рассчитал; массив и список были заполнены 5000 случайными (разными) строками.

private static void readArray(String[] strings) {
    long totalchars = 0;
    for (int j = 0; j < ITERATIONS; j++) {
        totalchars = 0;
        for (int i = 0; i < strings.length; i++) {
            totalchars += strings[i].length();

        }
    }
}

private static void readArrayList(List<String> stringsList) {
    long totalchars = 0;
    for (int j = 0; j < ITERATIONS; j++) {
        totalchars = 0;
        for (int i = 0; i < stringsList.size(); i++) {
            totalchars += stringsList.get(i).length();
        }
    }
}
6 голосов
/ 04 апреля 2009

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

5 голосов
/ 05 апреля 2009

Если у вас есть тысячи, подумайте об использовании trie. Три представляет собой древовидную структуру, которая объединяет общие префиксы сохраненной строки.

Например, если строки были

intern
international
internationalize
internet
internets

Три будет хранить:

intern
 -> \0
 international
 -> \0
 -> ize\0
 net
 ->\0
 ->s\0

Строки требуют 57 символов (включая нулевой терминатор, '\ 0') для хранения, плюс любой размер объекта String, который их содержит. (По правде говоря, нам, вероятно, следует округлить все размеры до кратных 16, но ...) Назовите это 57 + 5 = 62 байта, примерно.

Дереву требуется 29 (включая нулевой терминатор, '\ 0') для хранения, плюс размер узлов дерева, которые являются ссылкой на массив и список дочерних узлов дерева.

Для этого примера это, вероятно, примерно одинаково; для тысяч это, вероятно, будет меньше, если у вас есть общие префиксы.

Теперь при использовании trie в другом коде вам придется конвертировать в String, возможно, используя StringBuffer в качестве посредника. Если многие строки используются одновременно как строки, за пределами дерева это потеря.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...