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);
    }
}

结果

  • 普通递归:3ms
  • ForkJoinPool:25ms

这里的错误在哪里?

我只是想了解是否存在特定阈值,低于该阈值的普通递归比 ForkJoinPool 更快。

4

2 回答 2

8

生活中没有什么是免费的。如果您必须将一个啤酒箱从您的汽车移动到您的公寓 - 有什么更快的方法:手动将它搬运到那里,或者先去棚子,让独轮车用它来移动那个箱子?

创建线程对象是一种“本机”操作,它进入底层操作系统以获取那里的资源。这可能是一项相当昂贵的操作。

含义:只是在问题上抛出“更多线程”并不会自动加快速度。从相反的方面来说。当您的任务主要是 CPU 密集型任务时,并行处理可能会获得少量收益。当您执行大量 IO 时,拥有多个线程可以让您整体上“减少”等待;从而提高您的吞吐量。

换句话说:Fork/Join 在完成真正的工作之前需要相当多的活动。将它用于只需要几毫秒的计算简直是矫枉过正。因此:您将寻找适用于更大数据集的“fork/join”操作。

如需进一步阅读,您可以查看并行流。那些在幕后使用 fork/join 框架;令人惊讶的是,期望任意parallelStream流也比普通流“更快”是一种误解。

于 2017-04-16T05:27:42.097 回答
5

这有多个方面:

  1. 对于同一问题,串行(例如普通递归)和并行(例如 forkjoin)解决方案之间有区别吗?

  2. 并行文件系统访问的范围是什么?

  3. 衡量绩效的陷阱是什么?

回答#1。是,有一点不同。并行性不适用于太小的问题。使用并行解决方案,您需要考虑以下开销:

  • 创建和管理线程
  • 将信息从父线程传递给子线程
  • 将子线程的结果返回给父线程
  • 同步访问共享数据结构,
  • 等待最慢/最后完成的子线程完成。

这些在实践中如何发挥作用取决于多种因素……包括问题的规模以及并行性的机会。

#2 的答案(可能)没有你想象的那么多。典型的文件系统存储在具有物理特性的磁盘驱动器上,例如磁盘旋转和磁盘磁头搜索。这些通常会成为瓶颈,并且您拥有的高端存储系统越少,并行性的空间就越小。

#3 的答案是有很多陷阱。这些陷阱可能会导致非常误导(即无效)的性能结果....如果您不允许它们。最大的陷阱之一是 JVM 需要时间来“热身”。即加载类、进行JIT 编译、调整堆大小等等。

另一个适用于执行文件系统 I/O 的基准测试的陷阱是,典型的操作系统会执行诸如缓存磁盘块和文件/目录元数据之类的事情。因此,您第二次访问文件或目录时可能会比第一次更快。


话虽如此,如果您拥有设计良好的高性能文件系统(例如,SSD 上的 inode)和设计良好的应用程序,以及足够多的内核,则可以通过并行实现非凡的文件系统扫描率。(例如,在 12 小时内检查十亿个文件的修改时间戳......)

于 2017-04-16T05:35:39.623 回答