Java 8 потоков серийных и параллельных производительности

На моей машине программа ниже печатает:

OptionalLong[134043]
 PARALLEL took 127869 ms
OptionalLong[134043]
 SERIAL took 60594 ms

Мне непонятно, почему выполнение программы в серийном режиме происходит быстрее, чем выполнение ее параллельно. Я дал обе программы -Xms2g -Xmx2g в поле 8gb, которое относительно тихо. Может кто-то уточнить, что происходит?

import java.util.stream.LongStream;
import java.util.stream.LongStream.Builder;

public class Problem47 {

    public static void main(String[] args) {

        final long startTime = System.currentTimeMillis();
        System.out.println(LongStream.iterate(1, n -> n + 1).parallel().limit(1000000).filter(n -> fourConsecutives(n)).findFirst());
        final long endTime = System.currentTimeMillis();
        System.out.println(" PARALLEL took " +(endTime - startTime) + " ms");

        final long startTime2 = System.currentTimeMillis();
        System.out.println(LongStream.iterate(1, n -> n + 1).limit(1000000).filter(n -> fourConsecutives(n)).findFirst());
        final long endTime2 = System.currentTimeMillis();
        System.out.println(" SERIAL took " +(endTime2 - startTime2) + " ms");
    }

    static boolean fourConsecutives(final long n) {
        return distinctPrimeFactors(n).count() == 4 &&
                distinctPrimeFactors(n + 1).count() == 4 &&
                distinctPrimeFactors(n + 2).count() == 4 &&
                distinctPrimeFactors(n + 3).count() == 4;
    }

    static LongStream distinctPrimeFactors(long number) {
        final Builder builder = LongStream.builder();
        final long limit = number / 2;
        long n = number;
        for (long i = 2; i <= limit; i++) {
            while (n % i == 0) {
                builder.accept(i);
                n /= i;
            }
        }
        return builder.build().distinct();
    }

}

Ответ 1

Пока Брайан Гетц прав относительно вашей установки, например. что вы должны использовать .range(1, 1000000), а не .iterate(1, n -> n + 1).limit(1000000), и что ваш тестовый метод очень упрощен, я хочу подчеркнуть важный момент:

даже после устранения этих проблем, даже используя настенные часы и TaskManager, вы можете видеть, что что-то не так. На моей машине операция занимает около полуминутки, и вы можете видеть, что parallelism падает примерно на две секунды. Даже если специализированный инструмент сравнения может дать разные результаты, это не имело бы значения, если вы не хотите постоянно запускать свое окончательное приложение в тестовом инструменте...

Теперь мы могли бы попытаться высмеять больше о вашей настройке или сказать вам, что вам следует узнать о вещах о структуре Fork/Join, например, которые были созданы разработчиками список обсуждений.

Или мы попробуем альтернативную реализацию:

ExecutorService es=Executors.newFixedThreadPool(
                       Runtime.getRuntime().availableProcessors());
AtomicLong found=new AtomicLong(Long.MAX_VALUE);
LongStream.range(1, 1000000).filter(n -> found.get()==Long.MAX_VALUE)
    .forEach(n -> es.submit(()->{
        if(found.get()>n && fourConsecutives(n)) for(;;) {
            long x=found.get();
            if(x<n || found.compareAndSet(x, n)) break;
        }
    }));
es.shutdown();
try { es.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); }
catch (InterruptedException ex) {throw new AssertionError(ex); }
long result=found.get();
System.out.println(result==Long.MAX_VALUE? "not found": result);

На моей машине он делает то, что я ожидаю от параллельного выполнения, занимая чуть больше, чем ⟨sequential time⟩/⟨number of cpu cores⟩. Не изменяя ничего в вашей реализации fourConsecutives.

Суть в том, что, по крайней мере, когда обработка одного элемента занимает значительное время, текущая реализация Stream (или базовая структура Fork/Join) имеет проблемы, поскольку уже обсуждался в этом связанном вопросе. Если вы хотите надежный parallelism, я бы рекомендовал использовать проверенные и проверенные ExecutorService s. Как вы можете видеть в моем примере, это не означает, что вы отбрасываете возможности Java 8, они хорошо сочетаются. Только автоматическое parallelism, введенное с помощью Stream.parallel, должно использоваться с осторожностью (учитывая текущую реализацию).

Ответ 2

Мы можем упростить выполнение параллельно, но мы не можем сделать parallelism легко.

Преступник в вашем коде представляет собой комбинацию limit + parallel. Внедрение limit() тривиально для последовательных потоков, но довольно дорого для параллельных потоков. Это связано с тем, что определение предельной операции привязано к порядку встречи потока. Потоки с ограничением() часто медленнее параллельно, чем в последовательном, если только вычисление, сделанное для элемента, очень велико.

Выбор источника потока также ограничивает parallelism. Использование iterate(0, n->n+1) дает целые положительные числа, но iterate является фундаментально последовательным; вы не можете вычислить n-й элемент, пока не вычислите (n-1)-й элемент. Поэтому, когда мы пытаемся разделить этот поток, мы заканчиваем расщепление (во-первых, отдых). Попробуйте вместо этого использовать range(0,k); это расщепляется гораздо лучше, расщепляясь пополам со случайным доступом.