Почему функция удаления ArrayList Java, похоже, стоит так мало?



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



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



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



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



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



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

629   5  

5 ответов:

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

  • скопируйте нужные элементы в новый список
  • использовать 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 GHz, 3GB RAM) были:

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

поэтому удаление элементов по индексу из LinkedList была более чем в 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;
    }

}

копирование массива является довольно дорогостоящей операцией. Это делается на очень базовом уровне (его собственный статический метод 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

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

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

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

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

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

современный процессор может довольно быстро скопировать непрерывный блок из 1 МБ оперативной памяти в оперативную память.

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

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

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

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

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

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

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

доступ к памяти осуществляется быстро, и копирование фрагмента памяти, вероятно, достаточно быстро на современном оборудовании, что перемещение в Положение N занимает слишком много времени.

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

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


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

Comments

    Ничего не найдено.