Вы выполняете эти методы в первый раз, поэтому они запускаются в интерпретированном режиме. Чтобы ускорить их первое выполнение, оптимизатор должен заменить их во время работы (это называется заменой в стеке), что не всегда дает ту же производительность, что и при повторном вводе оптимизированного результата. Делать это одновременно кажется еще хуже, по крайней мере для Java 8, поскольку я получил совершенно другие результаты для Java 11.
Таким образом, первым шагом будет вставка явного вызова, например getFirstList(); getSecondList();
, чтобы увидеть, как он будет работать, когда не вызывается в первый раз.
Другим аспектом является сборка мусора. Некоторые JVM начинаются с небольшой начальной кучи и будут выполнять полный сборщик мусора при каждом расширении кучи, что влияет на все потоки.
Таким образом, второй шаг будет начинаться с -Xms1G
(или даже лучше, -Xms2G
), чтобы начать с разумного размера кучи для количества объектов, которые вы собираетесь создать.
Но обратите внимание, что 3-й шаг добавления промежуточных списков результатов в список окончательных результатов (который происходит последовательно в любом случае) оказывает значительное влияние на производительность.
Таким образом, третьим шагом будет замена построения окончательного списка на l3 = new ArrayList<>(l1.size() + l2.size())
для обоих вариантов, чтобы гарантировать, что список имеет соответствующую начальную емкость.
Сочетание этих шагов дало менее секунды для последовательного выполнения и менее половины секунды для многопоточного выполнения под Java 8.
Для Java 11, у которого была намного лучшая отправная точка, для которой требовалось около одной секунды только из коробки, эти улучшения дали менее существенное ускорение. Также кажется, что у этого кода намного больше потребление памяти для этого кода.