Почему функция удаления ArrayList Java, похоже, стоит так мало?
У меня есть функция, которая управляет очень большим списком, превышающим около 250 000 элементов. Для большинства из этих пунктов, он просто заменяет элемент в позиции X. Тем не менее, около 5% из них, его необходимо удалить их из списка.
использование LinkedList казалось наиболее очевидным решением, чтобы избежать дорогостоящих удалений. Однако, естественно, доступ к LinkedList по индексу становится все более медленным с течением времени. Стоимость здесь составляет минуты (и много их.)
использование итератора над этим LinkedList также дорого, поскольку мне, похоже, нужна отдельная копия, чтобы избежать проблем параллелизма итератора при редактировании этого списка. Стоимость здесь составляет минуты.
однако, вот где мой ум немного взорван. Если я перейду на ArrayList, он работает почти мгновенно.
для списка с 297515 элементами удаление 11958 элементов и изменение всего остального занимает 909 МС. я проверил, что полученный список действительно 285557 в размер, как и ожидалось, и содержит обновленную информацию, которая мне нужна.
Почему это так быстро? Я посмотрел на источник для ArrayList в JDK6, и он, похоже, использует функцию arraycopy, как и ожидалось. Я хотел бы понять, почему ArrayList работает так хорошо здесь, когда здравый смысл, казалось бы, указывает на то, что массив для этой задачи-ужасная идея, требующая сдвига нескольких сотен тысяч элементов.
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) будет дорогостоящим.
чтобы добавить немного соли и специй к этому:
- посмотреть обратите внимание на эффективность структуры данных массива и обратите внимание на производительность динамического массиваВикипедия записи, которые описывают вашу озабоченность.
- имейте в виду, что с помощью структуры памяти это требует непрерывной памяти требует, ну, непрерывной памяти. Это означает, что ваша виртуальная память должна будет иметь возможность выделять непрерывные куски. Или даже с Java, вы увидите, что ваш JVM счастливо идет вниз с неясным OutOfMemoryException принимая его причину в аварии низкого уровня.
Comments