多线程 ByteBuffers 比顺序慢?
Multi-threaded ByteBuffers slower than sequential?
我有一个巨大的字节数组需要处理。理论上,应该可以将工作分成偶数块并将它们分配给不同的线程以提高多核机器上的性能。
我为每个线程分配了一个ByteBuffer
,并分别处理了部分数据。即使我有 8 个逻辑处理器,最终性能也比单线程慢。这也是非常不一致的。有时,相同的输入处理速度会慢一倍或更多。这是为什么?数据首先加载到内存中,因此不再执行 IO
操作。
我使用 MappedByteBuffer
because it's faster than ByteBuffer.wrap()
:
分配我的 ByteBuffer
public ByteBuffer getByteBuffer() throws IOException
{
File binaryFile = new File("...");
FileChannel binaryFileChannel = new RandomAccessFile(binaryFile, "r").getChannel();
return binaryFileChannel.map(FileChannel.MapMode.READ_ONLY, 0, binaryFileChannel.size());
}
我使用 Executors
进行并发处理:
int threadsCount = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(threadsCount);
ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
for (ByteBufferRange byteBufferRange : byteBufferRanges)
{
Callable<String> task = () ->
{
performTask(byteBufferRange);
return null;
};
completionService.submit(task);
}
// Wait for all tasks to finish
for (ByteBufferRange ignored : byteBufferRanges)
{
completionService.take().get();
}
executorService.shutdown();
并发任务performTask()
使用它们自己的ByteBuffer
实例从缓冲区读取内存,进行计算等等。它们不同步、不写入或相互影响。有什么想法出了什么问题,或者这不是一个很好的并行化案例吗?
同样的问题存在于 ByteBuffer.wrap()
和 MappedByteBuffer
中。
如@EJP 所述,磁盘并不是真正的多线程,尽管 SSD 可能有帮助。映射缓冲区的目的是让您不必自己管理内存;让 OS 执行它,因为它的虚拟内存管理器和文件系统缓存将比将它移动到 Java 的堆中更快,并且可能比您编写的任何内存管理代码更快。
如果处理真的可以并行化,您可能最好让一个线程读取整个文件,将其分成块(可能采用某种中间数据格式),然后让您的执行程序处理这些块。文件读取线程可以 运行 与其他线程并发,因此您无需读取整个文件即可开始处理。
您可能想尝试将执行程序的数量设置为 cores - 1
,这样您就不会耗尽文件读取线程。这将使 OS 有机会在没有上下文切换的情况下将文件读取线程 运行 保持在单个内核上,这样您将获得良好的 IO 性能,同时使用其他内核进行 CPU 密集工作。
仅供参考,这就是 Apache Spark 的目标。如果您需要处理更大的文件或需要比单个系统更快的处理速度,您可能需要查看它。
我有一个巨大的字节数组需要处理。理论上,应该可以将工作分成偶数块并将它们分配给不同的线程以提高多核机器上的性能。
我为每个线程分配了一个ByteBuffer
,并分别处理了部分数据。即使我有 8 个逻辑处理器,最终性能也比单线程慢。这也是非常不一致的。有时,相同的输入处理速度会慢一倍或更多。这是为什么?数据首先加载到内存中,因此不再执行 IO
操作。
我使用 MappedByteBuffer
because it's faster than ByteBuffer.wrap()
:
public ByteBuffer getByteBuffer() throws IOException
{
File binaryFile = new File("...");
FileChannel binaryFileChannel = new RandomAccessFile(binaryFile, "r").getChannel();
return binaryFileChannel.map(FileChannel.MapMode.READ_ONLY, 0, binaryFileChannel.size());
}
我使用 Executors
进行并发处理:
int threadsCount = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(threadsCount);
ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
for (ByteBufferRange byteBufferRange : byteBufferRanges)
{
Callable<String> task = () ->
{
performTask(byteBufferRange);
return null;
};
completionService.submit(task);
}
// Wait for all tasks to finish
for (ByteBufferRange ignored : byteBufferRanges)
{
completionService.take().get();
}
executorService.shutdown();
并发任务performTask()
使用它们自己的ByteBuffer
实例从缓冲区读取内存,进行计算等等。它们不同步、不写入或相互影响。有什么想法出了什么问题,或者这不是一个很好的并行化案例吗?
同样的问题存在于 ByteBuffer.wrap()
和 MappedByteBuffer
中。
如@EJP 所述,磁盘并不是真正的多线程,尽管 SSD 可能有帮助。映射缓冲区的目的是让您不必自己管理内存;让 OS 执行它,因为它的虚拟内存管理器和文件系统缓存将比将它移动到 Java 的堆中更快,并且可能比您编写的任何内存管理代码更快。
如果处理真的可以并行化,您可能最好让一个线程读取整个文件,将其分成块(可能采用某种中间数据格式),然后让您的执行程序处理这些块。文件读取线程可以 运行 与其他线程并发,因此您无需读取整个文件即可开始处理。
您可能想尝试将执行程序的数量设置为 cores - 1
,这样您就不会耗尽文件读取线程。这将使 OS 有机会在没有上下文切换的情况下将文件读取线程 运行 保持在单个内核上,这样您将获得良好的 IO 性能,同时使用其他内核进行 CPU 密集工作。
仅供参考,这就是 Apache Spark 的目标。如果您需要处理更大的文件或需要比单个系统更快的处理速度,您可能需要查看它。