Почему Java-функция удаления ArrayList, кажется, стоит так дешево? - PullRequest
55 голосов
/ 23 мая 2011

У меня есть функция, которая управляет очень большим списком, превышающим около 250 000 элементов. Для большинства этих предметов он просто заменяет предмет в позиции x. Однако примерно для 5% из них он должен удалить их из списка.

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

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

Однако, здесь мой ум немного взорван. Если я перехожу на ArrayList, он запускается практически мгновенно.

Для списка с 297515 элементами удаление 11958 элементов и изменение всего остального занимает 909 мс. Я убедился, что полученный список действительно имеет размер 285557, как и ожидалось, и содержит обновленную информацию, которая мне нужна.

Почему это так быстро? Я посмотрел на источник ArrayList в JDK6, и он, как и ожидалось, использует функцию arraycopy. Мне бы очень хотелось понять, почему ArrayList работает так хорошо, когда здравый смысл указывает на то, что массив для этой задачи - ужасная идея, требующая перемещения нескольких сотен тысяч элементов.

Ответы [ 5 ]

29 голосов
/ 24 мая 2011

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

  • Скопируйте нужные элементы в новый список
  • Использование Iterator.remove()чтобы удалить ненужные элементы из ArrayList
  • Используйте Iterator.remove() для удаления нежелательных элементов из LinkedList
  • Сжатие списка на месте (перемещение требуемых элементовв нижние позиции)
  • Удалить по индексу (List.remove(int)) на ArrayList
  • Удалить по индексу (List.remove(int)) на LinkedList

Каждый раз, когда я заполнял список 100000 случайных экземпляров Point и использовал условие фильтра (на основе хеш-кода), которое будет принимать 95% элементов и отклонять оставшиеся 5% (та же пропорция, которая указана в вопросе, нос меньшим списком, потому что у меня не было времени запустить тест для 250000 элементов.)

А среднее время (на моем старом MacBook Pro: Core 2 Duo, 2,2 ГГц, 3 ГБ ОЗУ) было:

CopyIntoNewListWithIterator   :      4.24ms
CopyIntoNewListWithoutIterator:      3.57ms
FilterLinkedListInPlace       :      4.21ms
RandomRemoveByIndex           :    312.50ms
SequentialRemoveByIndex       :  33632.28ms
ShiftDown                     :      3.75ms

Таким образом, удаление элементов по индексу из LinkedList было болееn в 300 раз дороже, чем удаление их из ArrayList, и, вероятно, где-то в 6000-10000 раз дороже, чем другие методы (которые исключают линейный поиск и arraycopy)

Здесь, кажется, нетбольшая разница между четырьмя более быстрыми методами, но я снова запустил только эти четыре со списком из 500000 элементов со следующими результатами:

CopyIntoNewListWithIterator   :     92.49ms
CopyIntoNewListWithoutIterator:     71.77ms
FilterLinkedListInPlace       :     15.73ms
ShiftDown                     :     11.86ms

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

Вот код:

import java.awt.Point;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

public class ListBenchmark {

    public static void main(String[] args) {
        Random rnd = new SecureRandom();
        Map<String, Long> timings = new TreeMap<String, Long>();
        for (int outerPass = 0; outerPass < 10; ++ outerPass) {
            List<FilterStrategy> strategies =
                Arrays.asList(new CopyIntoNewListWithIterator(),
                              new CopyIntoNewListWithoutIterator(),
                              new FilterLinkedListInPlace(),
                              new RandomRemoveByIndex(),
                              new SequentialRemoveByIndex(),
                              new ShiftDown());
            for (FilterStrategy strategy: strategies) {
                String strategyName = strategy.getClass().getSimpleName();
                for (int innerPass = 0; innerPass < 10; ++ innerPass) {
                    strategy.populate(rnd);
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        if (totalTime == null) totalTime = 0L;
                        timings.put(strategyName, totalTime - System.currentTimeMillis());
                    }
                    Collection<Point> filtered = strategy.filter();
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        timings.put(strategy.getClass().getSimpleName(), totalTime + System.currentTimeMillis());
                    }
                    CHECKSUM += filtered.hashCode();
                    System.err.printf("%-30s %d %d %d%n", strategy.getClass().getSimpleName(), outerPass, innerPass, filtered.size());
                    strategy.clear();
                }
            }
        }
        for (Map.Entry<String, Long> e: timings.entrySet()) {
            System.err.printf("%-30s: %9.2fms%n", e.getKey(), e.getValue() * (1.0/25.0));
        }
    }

    public static volatile int CHECKSUM = 0;

    static void populate(Collection<Point> dst, Random rnd) {
        for (int i = 0; i < INITIAL_SIZE; ++ i) {
            dst.add(new Point(rnd.nextInt(), rnd.nextInt()));
        }
    }

    static boolean wanted(Point p) {
        return p.hashCode() % 20 != 0;
    }

    static abstract class FilterStrategy {
        abstract void clear();
        abstract Collection<Point> filter();
        abstract void populate(Random rnd);
    }

    static final int INITIAL_SIZE = 100000;

    private static class CopyIntoNewListWithIterator extends FilterStrategy {
        public CopyIntoNewListWithIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            ArrayList<Point> dst = new ArrayList<Point>(list.size());
            for (Point p: list) {
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class CopyIntoNewListWithoutIterator extends FilterStrategy {
        public CopyIntoNewListWithoutIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            ArrayList<Point> dst = new ArrayList<Point>(inputSize);
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class FilterLinkedListInPlace extends FilterStrategy {
        public String toString() {
            return getClass().getSimpleName();
        }
        FilterLinkedListInPlace() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (Iterator<Point> it = list.iterator();
                 it.hasNext();
                 ) {
                Point p = it.next();
                if (! wanted(p)) it.remove();
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class RandomRemoveByIndex extends FilterStrategy {
        public RandomRemoveByIndex() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class SequentialRemoveByIndex extends FilterStrategy {
        public SequentialRemoveByIndex() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class ShiftDown extends FilterStrategy {
        public ShiftDown() {
            list = new ArrayList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            int outputSize = 0;
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) {
                    list.set(outputSize++, p);
                }
            }
            list.subList(outputSize, inputSize).clear();
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

}
17 голосов
/ 23 мая 2011

Копирование массива - довольно недорогая операция.Это сделано на самом базовом уровне (это Java-статический метод), и вы еще не достигли того уровня, когда производительность становится действительно важной.

В вашем примере вы копируете примерно 12000 раз массив размером 150000 (в среднем).Это не займет много времени.Я протестировал его здесь на своем ноутбуке, и это заняло менее 500 мс.

Обновление Я использовал следующий код для измерения на своем ноутбуке (Intel P8400)

import java.util.Random;

public class PerformanceArrayCopy {

    public static void main(String[] args) {

        int[] lengths = new int[] { 10000, 50000, 125000, 250000 };
        int[] loops = new int[] { 1000, 5000, 10000, 20000 };

        for (int length : lengths) {
            for (int loop : loops) {

                Object[] list1 = new Object[length];
                Object[] list2 = new Object[length];

                for (int k = 0; k < 100; k++) {
                    System.arraycopy(list1, 0, list2, 0, list1.length);
                }

                int[] len = new int[loop];
                int[] ofs = new int[loop];

                Random rnd = new Random();
                for (int k = 0; k < loop; k++) {
                    len[k] = rnd.nextInt(length);
                    ofs[k] = rnd.nextInt(length - len[k]);
                }

                long n = System.nanoTime();
                for (int k = 0; k < loop; k++) {
                    System.arraycopy(list1, ofs[k], list2, ofs[k], len[k]);
                }
                n = System.nanoTime() - n;
                System.out.print("length: " + length);
                System.out.print("\tloop: " + loop);
                System.out.print("\truntime [ms]: " + n / 1000000);
                System.out.println();
            }
        }
    }
}

Некоторые результаты:

length: 10000   loop: 10000 runtime [ms]: 47
length: 50000   loop: 10000 runtime [ms]: 228
length: 125000  loop: 10000 runtime [ms]: 575
length: 250000  loop: 10000 runtime [ms]: 1198
10 голосов
/ 23 мая 2011

Я думаю, что разница в производительности, скорее всего, сводится к тому, что ArrayList поддерживает произвольный доступ, а LinkedList - нет.

Если я хочу получить (1000) из ArrayList, я указываю определенный индекс для доступа к нему, однако LinkedList не поддерживает это, поскольку он организован через ссылки на узлы.

Если я вызову get (1000) из LinkedList, он будет перебирать весь список до тех пор, пока не найдет индекс 1000, и это может быть непомерно дорого, если в LinkedList есть большое количество элементов.

6 голосов
/ 23 мая 2011

Я специально пропускаю некоторые детали реализации, просто чтобы объяснить принципиальное отличие.

Чтобы удалить N-й элемент из списка из M элементов, LinkedListРеализация перейдет к этому элементу, затем просто удалит его и обновит указатели элементов N-1 и N + 1 соответственно.Эта вторая операция очень проста, но она подходит к этому элементу, который стоит вам времени.

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

  • перераспределить новый массив из M - 1 элементов
  • положить все от 0 до N - 1по индексу 0 в массиве нового архива
  • поместите все N + 1 в М с индексом N в массиве массива.

Думая об этом, вы заметите, что можете даже использовать повторнотот же массив, что и в Java, может использовать ArrayList с предварительно назначенными размерами, поэтому, если вы удалите элементы, вы также можете пропустить шаги 1 и 2 и напрямую выполнить шаг 3 и обновить свой размер.

Быстрый доступ к памяти ина современном оборудовании копирование фрагмента памяти, вероятно, выполняется достаточно быстро, поэтому перемещение в позицию N занимает слишком много времени.

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

Но ясно, что в длинном списке выполнение простого удаления (i) будет дорогостоящим.


Чтобы добавить к этому немного соли и специй:

6 голосов
/ 23 мая 2011

Интересные и неожиданные результаты. Это всего лишь гипотеза, но ...

В среднем для удаления одного из элементов массива потребуется переместить половину вашего списка (все после него) назад на один элемент. Если каждый элемент является 64-битным указателем на объект (8 байт), то это означает, что копируется 125000 элементов x 8 байт на указатель = 1 МБ.

Современный процессор может довольно быстро скопировать непрерывный блок 1 МБ ОЗУ в ОЗУ.

По сравнению с циклическим просмотром связанного списка для каждого доступа, который требует сравнений и ветвлений, а также других недружественных действий ЦП, копирование ОЗУ происходит быстро.

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

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