ForkJoinPool против простой рекурсии

Прочитав о ForkJoinPool, я попытался провести эксперимент, чтобы проверить, насколько быстро работает ForkJoinPool по сравнению с простой рекурсией.

Я рекурсивно подсчитал количество файлов в папке, и, к моему удивлению, обычная рекурсия работала намного лучше, чем ForkJoinPool

Вот мой код.

Рекурсивная задача

class DirectoryTask extends RecursiveTask<Long> {

    private Directory directory;

    @Override
    protected Long compute() {
        List<RecursiveTask<Long>> forks = new ArrayList<>();
        List<Directory> directories = directory.getDirectories();
        for (Directory directory : directories) {
            DirectoryTask directoryTask = new DirectoryTask(directory);
            forks.add(directoryTask);
            directoryTask.fork();
        }
        Long count = directory.getDoumentCount();
        for (RecursiveTask<Long> task : forks) {
            count += task.join();
        }
        return count;
    }
}

Простая рекурсия

private static Long getFileCount(Directory directory) {
        Long recursiveCount = 0L;
        List<Directory> directories = directory.getDirectories();
        if (null != directories) {
            for (Directory d : directories) {
                recursiveCount += getFileCount(d);
            }
        }
        return recursiveCount + directory.getDoumentCount();
    }

Объект каталога

class Directory {

    private List<Directory> directories;
    private Long doumentCount = 0L;

    static Directory fromFolder(File file) {
        List<Directory> children = new ArrayList<>();
        Long documentCount = 0L;
        if (!file.isDirectory()) {
            throw new IllegalArgumentException("Only directories are allowed");
        }
        String[] files = file.list();
        if (null != files) {
            for (String path : files) {
                File f = new File(file.getPath() + File.separator + path);
                if (f.isHidden()) {
                    continue;
                }
                if (f.isDirectory()) {
                    children.add(Directory.fromFolder(f));
                } else {
                    documentCount++;
                }
            }
        }
        return new Directory(children, documentCount);
    }
}

Результаты

  • Обычная рекурсия: 3 мс
  • ForkJoinPool: 25 мс

Где здесь ошибка?

Я просто пытаюсь понять, существует ли определенный порог, ниже которого простая рекурсия работает быстрее, чем ForkJoinPool.


person Rachit Sachdeva    schedule 16.04.2017    source источник


Ответы (2)


Ничто в жизни не дается бесплатно. Если вам нужно было перевезти один ящик из-под пива из машины в квартиру - что быстрее: нести его туда вручную или сначала пойти в сарай, чтобы тачка на ней перевезла этот ящик?

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

Значение: просто добавление «большего количества потоков» для решения проблемы автоматически не ускоряет процесс. Иначе. Когда ваша задача в основном интенсивно использует ЦП, параллельная работа может дать небольшой выигрыш. Когда вы выполняете много операций ввода-вывода, наличие нескольких потоков позволяет вам в целом делать «меньше» ожиданий; тем самым повышая пропускную способность.

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

Для дальнейшего чтения вы можете посмотреть параллельные потоки. Те используют структуру fork/join под прикрытием; и, к удивлению, ошибочно ожидать, что произвольный parallelStream будет "быстрее", чем обычные потоки.

person GhostCat    schedule 16.04.2017
comment
Большое спасибо за разъяснение. Итак, что я понимаю из обоих ответов здесь (один из ответов Стивена С), ForkJoinPool полезен, когда выполняемая задача фактически занимает некоторое время для завершения, чтобы компенсировать накладные расходы на создание потока . - person Rachit Sachdeva; 16.04.2017
comment
Верный. Помимо этого: вы также должны быть осторожны, как/что вы оцениваете. Меня не удивит, если время выполнения изменится, если вы повторите свои эксперименты несколько раз. - person GhostCat; 16.04.2017

Здесь есть несколько аспектов:

  1. Есть ли разница между последовательными (например, простой рекурсией) и параллельными (например, forkjoin) решениями одной и той же проблемы?

  2. Каковы возможности распараллеливания доступа к файловой системе?

  3. Каковы ловушки для измерения эффективности?

Ответ на №1. Да есть разница. Параллелизм не годится для слишком маленькой задачи. При использовании параллельного решения необходимо учитывать накладные расходы на:

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

Как они работают на практике, зависит от множества вещей... включая размер проблемы и возможности параллелизма.

Ответ на № 2 (вероятно) не так много, как вы думаете. Типичная файловая система хранится на диске с такими физическими характеристиками, как вращение диска и поиск головки диска. Обычно они становятся узким местом, и если у вас нет высокопроизводительной системы хранения, возможности для параллелизма невелики.

Ответ на № 3 заключается в том, что есть много ловушек. И эти ловушки могут привести к очень вводящим в заблуждение (то есть недействительным) результатам производительности .... если вы их не допускаете. Одна из самых больших ловушек заключается в том, что JVM требуется время, чтобы «разогреться»; то есть загружать классы, выполнять JIT-компиляцию, изменять размер кучи и т. д.

Еще одна ловушка, связанная с эталонными тестами, выполняющими ввод-вывод файловой системы, заключается в том, что типичная ОС будет выполнять такие действия, как кэширование дисковых блоков и метаданных файлов/каталогов. Таким образом, второй раз, когда вы обращаетесь к файлу или каталогу, он, скорее всего, будет быстрее, чем в первый раз.


Сказав это, если у вас есть хорошо спроектированная, высокопроизводительная файловая система (например, иноды, хранящиеся на твердотельных накопителях), хорошо спроектированное приложение и достаточное количество ядер, можно достичь необычайной скорости сканирования файловой системы за счет параллелизма. (Например, проверка меток времени модификации полумиллиарда файлов менее чем за 12 часов....)

person Stephen C    schedule 16.04.2017