Улучшить время отклика Java/Stream/фильтр

Я сравниваю два списка объектов, используя потоки (filter/anyMatch). Размер двух списков может достигать миллиона объектов.

Я провел тест с приведенным ниже кодом. Часто размер двух списков близок.

Как я могу улучшить время отклика приведенного ниже кода?

List<String> listDifferences = list1.stream()
            .filter(o1 -> list2.stream()
                    .noneMatch(o2 -> o2.getValue().equals(o1.getValue())))
            .map(ObjectValue::getValue).collect(Collectors.toList());

Время отклика для 699839 объектов для каждого списка:

Fri May 10 16:14:09 CEST 2024
processing ....
Fri May 10 16:33:30 CEST 2024

1 миллион*1 миллион = 1 триллион. Вы делаете ~1 триллион операций.

Michael 10.05.2024 17:13
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
1
96
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это не то, о чем говорит производительность.

Тот факт, что вы сосредоточили вопрос на «потоках» / «фильтре», позволяет предположить, что вы думаете, что возня с тем, как вы перебираете коллекции, каким-то образом оказывает измеримое влияние на производительность. Или, что еще хуже, вы думаете, что потоки «просто лучше», а именно «быстрее».

Это все неправильно.

Дело не в том, как вы перебираете эти вещи. Все происходит одинаково быстро (или, в данном случае, одинаково медленно). И потоки, во всяком случае, медленнее. Речь идет о читабельности (к тому же они часто менее читабельны. Это инструмент в наборе инструментов. Используйте его, когда это уместно. Зачастую это неуместно. Если вы думаете: «Потоки… это… лучше… потому что… они потоки!" - это очень вредный мыслительный процесс. Хороший код хорош, потому что он читаем/понятен (это означает: читатель, который не очень хорошо знаком с ним, сделает вывод о том, что он делает и насколько быстрее, чем альтернативные способы его написания). , и этот вывод верен), легко тестируется и гибок (в условиях ожидаемых запросов на изменение он легко адаптируется для выполнения этих запросов).

если написание его с помощью потоков приводит к лучшим «оценкам» по этим факторам, то потоки лучше. А если нет, то их нет. На самом деле ни в каком стиле речь не идет о том, что «этот стиль лучше, чем тот». Конечно, не «использовать потоки».

Итак, о чем идет речь: алгоритмическая сложность

То, что вы на самом деле ищете, не имеет ничего общего с потоками. Это связано с тем, как спроектированы структуры данных. Вы ищете концепцию алгоритмической сложности. Обычно это выражается в виде «нотации большого О» — возможно, вы слышали этот термин.

Учитывая алгоритм, который работает с неизвестным, но счетным количеством записей, давайте обработаем такое количество записей, что характеристика производительности того, сколько времени требуется (или сколько памяти потребляется для выполнения задания), начинает формировать математическую зависимость относительно количества записей. мы обрабатываем. Обозначение big-O выражает это математическое соотношение. Это просто относится к тому, «при условии, что входной набор достаточно велик, сколько времени займет процесс, когда вы увеличиваете или уменьшаете этот входной набор в относительном выражении». Он не делает никаких заявлений о производительности конкретного процессора, и, по сути, в том, как работают компьютеры, для данного алгоритма можно обеспечить конкретное математическое соотношение, которое справедливо для любой архитектуры и любой ОС.

Например, ваш алгоритм — O(n*m), где n обозначает размер входного списка 1, а m — входной список 2. Чтобы упростить ситуацию, мы могли бы сказать, что предполагается, что оба списка примерно равны по размеру, что делает это O(n^2).

Другими словами, если, скажем, вы обнаружите, что для n=10,000 это занимает 10 секунд, то для n=20,000 ожидается, что это займет 10 * 10 = 100 секунд или около того. Затрачиваемое на это время растет в квадрате по мере того, как количество обрабатываемых элементов растет линейно. Это не здорово.

Не имеет значения, как вы обрабатываете данные (с помощью циклов for или списков). Не имеет значения, на каком процессоре вы это запускаете или на каком языке программирования вы это пишете. Характеристика O(n^2) является характеристикой этого алгоритма.

Тогда исправление — использовать другой алгоритм!

Правильный алгоритм: Наборы

Это на порядки быстрее, потому что у него совсем другая характеристика производительности — его характеристика O(n+m), которая сводится просто к O(n), если мы говорим, что списки примерно одинакового размера. Поскольку сложность алгоритма связана с относительными мерами, постоянные факторы не имеют значения (таким образом, O(2n) — это просто O(n)).

Это потому, что сначала мы добавляем все элементы в списке 1 в набор (это занимает O(n) времени - удвоение входного размера удваивает время обработки; в отличие от вашего алгоритма, где удвоение входного размера экспоненциально увеличивает время обработки). Затем для каждого элемента во втором списке мы спрашиваем набор, содержит ли он элемент O(1). Таким образом, эта операция O(m). Запустите их последовательно, и мы доберемся до O(n+m), что упрощается до O(n):

var set1 = new HashSet<String>(list1); // O(n)
var set2 = new HashSet<String>(list2); // O(m)
set1.removeAll(set2); // O(n)

... and I gues if you must have your answer in list form:
return new ArrayList<String>(set1); // O(n)

Это куча O(n) в последовательности, так что всего O(n).

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

На самом деле ваш код использует подсвойство (.getValue()), а не сам объект. Это не особая проблема.

var set1 = new HashSet<String>();
for (MyObj o1 : list1) set1.add(o1.getValue());
var set2 = new HashSet<String>();
for (MyObj o2 : list2) set2.add(o2.getValue());
set1.removeAll(set2);
return new ArrayList<String>(set1);

Примечание. Наборы имеют O(1) производительность, если их элементы имеют правильное распределение хеша. Нет никаких оснований думать, что у вас возникнут проблемы в связи с этим вопросом.

@YKmar "THANKS A LOT FOR YOUR ANSWER" На StackOverflow принятие ответа является более предпочтительным выражением благодарности!

WJS 10.05.2024 21:31

Для операции не нужно собирать два комплекта. Вместо того, чтобы создавать set2, вы можете сначала вызвать remove на set1. var set1 = new HashSet<String>(); for(MyObj o1: list1) set1.add(o1.getValue()); for(MyObj o2: list2) set1.remove(o2.getValue()); /* done. optionally: */ new ArrayList<String>(set1)

Holger 17.05.2024 17:37

Другие вопросы по теме