ForkJoinPool与普通递归的比较

3

阅读了关于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更快。

2个回答

8

生活中没有什么是免费的。如果你不得不把一个啤酒箱从你的车子里搬到公寓里,哪个更快:手动搬运还是先去小屋取手推车来移动一个啤酒箱?

创建线程对象是一种 "本地" 操作,它会深入底层操作系统以获取资源。这可能是一项相当昂贵的操作。

意思是:仅仅对问题投入“更多线程”并不能自动加速事情。相反地,当你的任务主要是 CPU 密集型时,可能很少从并行处理中获益。当你需要做大量的 IO 时,拥有多个线程可以让你总体上等待“较少”,从而提高吞吐量。

换句话说:Fork/Join 在实际工作之前需要相当多的活动。将其用于只需要几毫秒的计算是过度杀伤力。因此,你将寻找在较大数据集上工作的 "fork/join" 操作。

进一步阅读,你可以看看 并行流。这些使用了 fork/join 框架; 让人惊讶的是,期望任意的 parallelStream 比普通流更快是一种误解。


非常感谢您的澄清。根据这里两个答案(其中一个来自Stephen C)的理解,当执行的任务实际上需要花费一些时间来完成时,ForkJoinPool是有益的,以补偿线程创建的开销。 - Rachit Sachdeva
正确。除此之外:您还必须小心如何/什么进行基准测试。如果您多次重复实验,执行时间发生变化也不足为奇。 - GhostCat

5
有多个方面需要考虑:
1. 相同问题的串行解决方案(如纯递归)和并行解决方案(如 forkjoin)之间是否存在差异?
答案是有区别的。对于一个太小的问题,并行化并不好。使用并行解决方案时,需要考虑以下开销:创建和管理线程、从父线程传递信息到子线程、从子线程返回结果到父线程、共享数据结构的同步访问,以及等待最慢/最后完成的子线程完成等。
在实践中,上述因素的影响取决于多种因素,包括问题的大小和并行化的机会。
2. 并行化文件系统访问的范围是多少?
答案(可能)不像您想象的那么大。典型的文件系统存储在具有物理特征(例如磁盘旋转和磁盘头寻道)的磁盘驱动器上。这些通常会成为瓶颈,除非您有高端存储系统,否则就没有太多并行化的空间。
3. 测量性能的陷阱有哪些?
陷阱有很多。如果不考虑这些陷阱,它们可以导致非常误导性(即无效)的性能结果。其中最大的陷阱之一是JVM需要时间来“热身”,即加载类、进行JIT编译、调整堆大小等。
另一个适用于文件系统I/O基准测试的陷阱是典型操作系统会缓存磁盘块和文件/目录元数据。因此,第二次访问文件或目录通常比第一次快。
总之,如果您具有设计良好的高性能文件系统(例如在SSD上保存索引节点)和设计精良的应用程序,并且拥有足够的核心,就可以通过并行化实现对文件系统扫描的非凡速度。(例如,在不到12小时内检查了超过50亿个文件的修改时间戳...)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接