Есть ли причина не использовать Java 8 parallelSort?
Я читал этот вопрос о различиях между Java Arrays.sort
и Arrays.parallelSort
, которому уже несколько лет. Что меня удивило, так это то, что был только один вопрос, в котором упоминалось о недостатках использования parallelSort
; а именно, что ускорение уменьшается, если вы используете много вашего процессора.
Предполагая, что вы не находитесь в какой-то специализированной однопоточной среде, всегда ли следует выбирать parallelSort
сортировку? Есть ли причина не делать этого? Обратите внимание, что в одном из ответов на вопрос, приведенный выше, упоминается, что если имеется менее 4096 элементов, parallelSort
любом случае просто вызывает sort
.
Ответы
Ответ 1
Есть некоторые недостатки использования Arrays.parallelSort
- он использует
ForkJoinPool.commonPool()
и будет бороться с другими функциями, которые используют его по умолчанию (например, parallel()
в потоке) -
Arrays.parallelSort
используемый в пуле Arrays.parallelSort
не настраивается (только на глобальном уровне путем увеличения количества потоков в общих пулах) - он работает хуже на небольших наборах данных (чаще всего массивы содержат мало элементов, JDK даже признает, что, например, большинство
ArrayList
остаются пустыми в течение всего срока их службы, что экономит довольно много памяти и времени ЦП, не создавая экземпляров массивов, которые никогда не будут заполнены )
И еще один случайный сценарий: скажем, если вы реализуете какую-то карточную игру, которая требует сортировки. Смущающе легко распараллеливать несколько игровых исполнений рядом друг с другом, вместо того чтобы распараллеливать механизм сортировки одного прогона, который может занимать лишь часть всего игрового цикла. Вы потеряли простой способ распараллеливания сейчас (например, при запуске игры в контексте генетических алгоритмов).
Но да, если у вас большие массивы и сортировка является существенной частью времени выполнения ваших приложений, используйте Arrays.parallelSort
.
РЕДАКТИРОВАТЬ: И даже если Arrays.parallelSort
переключается на нормальную сортировку, если данный массив содержит менее 4096 элементов: все это о намерении - вы хотите параллельную сортировку, если это возможно, которая имеет другое значение, чем просто вызов sort
. И быть придирчивым: он действительно работает хуже на небольших массивах, так как он должен выполнить дополнительную проверку, если массив содержит менее 4096 элементов, и некоторые другие проверки о количестве потоков в общих пулах (эти накладные расходы, конечно, незначительны) :),
Ответ 2
Это мало чем отличается от вопроса о том, когда использовать stream()
и parallelStream()
- это зависит от того, сколько у вас данных. Конечно, большую часть времени при параллельной сортировке 10 элементов будет занимать многопоточная структура, которая находится под колпаком (которая не указана в документации), а не сама сортировка.
Но вы также должны задаться вопросом, почему такие методы введены IMO. Аппаратное обеспечение движется (уже переместилось?) Ко многим процессорам, не более GHz
, поэтому параллельная работа - это нормальный курс для любого языка, который хочет остаться живым в течение следующих 20 лет.
Относительно того, сколько данных вам нужно для обеспечения parallelSort
sort
, а не sort
, плюс знание того, что нам нужно как минимум MIN_ARRAY_SORT_GRAN + 1
чтобы получить потенциальную выгоду; написание правильного теста, чтобы доказать, что для этой конкретной установки и запуска вам понадобится хотя бы число X
, не так уж сложно. Вы также должны принять во внимание, что некоторые массивы могут быть уже отсортированы (объяснено далее), в то время как некоторые могут быть полностью не отсортированы (например, 5,4,3,2,1
), это влечет за собой штрафы за второй.
Взяв некоторые случайные данные и сделав тест:
@Warmup(iterations = 10)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
public class ParallelSort {
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(ParallelSort.class.getName())
.build();
new Runner(opt).run();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public int[] parallel(ParallelSortExecutionPlan plan) {
Arrays.parallelSort(plan.ints());
return plan.ints();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public int[] nonParallel(ParallelSortExecutionPlan plan) {
Arrays.sort(plan.ints());
return plan.ints();
}
}
@State(Scope.Benchmark)
public class ParallelSortExecutionPlan {
@Param(value = {"10", "100", "1000", "10000", "100000", "1000000"})
private int howMany;
private int[] ints;
public static void main(String[] args) {
}
@Setup(Level.Invocation)
public void setUp() {
ints = new int[howMany];
for (int i = 0; i < howMany; ++i) {
ints[i] = ThreadLocalRandom.current().nextInt();
}
}
int[] ints() {
return ints;
}
}
Просто обратите внимание, что второй класс использует @Setup(Level.Invocation)
(если вы немного знакомы с JMH) - здесь это очень острый инструмент; но я использую его, потому что я хочу несортированный массив для каждого Invocation
метода. В противном случае, если бы, например, использовался Trial
- только первый вызов был бы @Benhcmark
массивом, все остальные вызовы метода @Benhcmark
уже были бы отсортированы. Для удовольствия, вы можете изменить одну строку на @Setup(Level.Trial)
например, и посмотреть результаты, они будут иметь мало смысла.
Запуск этого показывает:
Benchmark (howMany) Mode Cnt Score Error Units
ParallelSort.nonParallel 10 avgt 2 128.847 ns/op
ParallelSort.parallel 10 avgt 2 116.656 ns/op
ParallelSort.nonParallel 100 avgt 2 1956.746 ns/op
ParallelSort.parallel 100 avgt 2 1963.335 ns/op
ParallelSort.nonParallel 1000 avgt 2 32162.611 ns/op
ParallelSort.parallel 1000 avgt 2 31716.915 ns/op
ParallelSort.nonParallel 10000 avgt 2 423531.663 ns/op
ParallelSort.parallel 10000 avgt 2 201802.609 ns/op
ParallelSort.nonParallel 100000 avgt 2 6503511.987 ns/op
ParallelSort.parallel 100000 avgt 2 1363169.661 ns/op
ParallelSort.nonParallel 1000000 avgt 2 69058738.586 ns/op
ParallelSort.parallel 1000000 avgt 2 13469112.930 ns/op
Довольно ожидаемый результат для меня.
Ответ 3
Нет, я бы сказал нет для достаточно маленьких массивов. Накладные расходы на настройку потоков не приведут к заметному ускорению.
Ключ "достаточно мал". Это не будет одинаковым ответом на все проблемы.
Догма никогда не должна применяться, кроме как в случае этого правила догмы. Также как единственное, что мы никогда не должны терпеть, это нетерпимость. Там где-то есть парадокс Поппера.
Ответ 4
В дополнение к таким причинам, как общее использование пула и минимальный размер, который можно оптимизировать, вам также может не потребоваться распараллеливать одну сортировку, если у вас обычно много транзакций, требующих параллельной сортировки.
В этом сценарии вы можете избежать накладных расходов, разделив рабочие пакеты. (Однако наличие контролируемого исполнителя с настраиваемой параллельной работой работает и для многопоточной отправки - вы просто увеличиваете количество припаркованных потоков и переключателей контекста)