当使用FileInputStream时,如何确定理想的缓冲区大小?

177

我有一个方法,可以从文件创建MessageDigest(哈希),我需要对很多文件(>= 100,000)执行此操作。为了最大化性能,我应该将用于从文件读取的缓冲区大小设置多大?

大多数人都熟悉基本代码(我会在这里重复一遍,以防万一):

MessageDigest md = MessageDigest.getInstance( "SHA" );
FileInputStream ios = new FileInputStream( "myfile.bmp" );
byte[] buffer = new byte[4 * 1024]; // what should this value be?
int read = 0;
while( ( read = ios.read( buffer ) ) > 0 )
    md.update( buffer, 0, read );
ios.close();
md.digest();

最大化吞吐量的理想缓冲区大小是多少?我知道这取决于系统,而且我相当确定它也取决于操作系统、文件系统、硬盘驱动器以及可能涉及到其他硬件/软件。

(我应该指出我对Java有些陌生,所以这可能只是我不知道的一些Java API调用。)

编辑: 我事先不知道将在哪种系统上使用它,因此我不能做太多假设。(正因为这个原因我使用Java。)

编辑: 上面的代码省略了一些像 try..catch 这样使帖子更小的东西。

10个回答

237

最佳缓冲区大小与多个因素相关:文件系统块大小、CPU缓存大小和缓存延迟。

大多数文件系统配置为使用块大小为4096或8192。理论上,如果您将缓冲区大小配置为比磁盘块多几个字节,那么与文件系统的操作可能会非常低效(例如,如果您将缓冲区配置为每次读取4100个字节,则每个读取将需要文件系统进行2个块读取)。如果块已经在缓存中,则您需要支付 RAM->L3/L2 缓存延迟的代价。如果您不幸的是块还未在缓存中,则还需支付磁盘->RAM 延迟的代价。

这就是为什么大多数缓冲区的大小都是2的幂,并且通常大于或等于磁盘块大小的原因。这意味着你的一个流读取可能会导致多个磁盘块读取,但这些读取将始终使用完整的块-没有浪费的读取。

在典型的流场景中,这在很大程度上被抵消了,因为从磁盘读取的块在下一次读取时仍然在内存中(毕竟我们这里是顺序读取)-因此您在下一次读取时需要支付 RAM->L3/L2 缓存延迟的代价,但不需要磁盘->RAM 延迟。从数量级上看,磁盘->RAM 延迟非常慢,它几乎压倒了您可能遇到的任何其他延迟。

因此,我怀疑如果您使用不同的缓存大小进行测试(我自己还没有这样做),那么您可能会发现缓存大小对文件系统块大小有很大影响。在那之上,我认为事情会很快稳定下来。

这里有很多条件和例外-系统的复杂性实际上相当惊人(仅仅掌握 L3 -> L2 缓存传输就非常复杂,并且随着每种CPU类型而变化)。

这就导致了一个“现实世界”的答案:如果你的应用程序与大多数应用程序相似,将缓存大小设置为8192并继续(更好的选择是选择封装而不是性能,并使用BufferedInputStream隐藏细节)。如果您的应用程序属于高度依赖磁盘吞吐量的1%,则应该设计您的实现以便可以交换不同的磁盘交互策略,并提供旋钮和控制器以允许用户测试和优化(或者采用某些自我优化系统)。


3
我在一部手机(Nexus 5X)上对我的Android应用程序进行了基准测试,包括小文件(3.5Mb)和大文件(175 Mb)。我发现黄金大小将是长度为524288的byte[]。如果您根据文件大小在小缓冲区4Kb和大缓冲区524Kb之间切换,您可能会节省10-20毫秒,但这并不值得。因此,在我的情况下,524 Kb是最佳选项。 - Kirill Karmazin

23

是的,这可能取决于各种因素-但我怀疑这不会产生太大的影响。我倾向于选择16K或32K作为内存使用和性能之间的良好平衡。

请注意,在代码中应该有一个try/finally块,以确保即使抛出异常也要关闭流。


1
我编辑了有关try..catch的帖子。实际代码中,我有一个try..catch,但为了让帖子更简洁,我将其省略了。 - ARKBAN
1
如果我们想要为其定义一个固定的大小,哪个大小更好?4k、16k还是32k? - BattleTested_закалённый в бою
3
请不要在评论中纠缠用户。你在第二次评论前等待的时间少于一小时。请记住,用户可能正在睡觉、开会或者忙于其他事情,他们没有义务回复评论。但是为了回答你的问题:这完全取决于上下文。如果你在运行非常内存受限的系统,你可能需要一个小缓冲区。如果你在运行大型系统,使用更大的缓冲区将减少读取调用的数量。Kevin Day的回答很好。 - Jon Skeet

9
在大多数情况下,它并不是那么重要。只需选择一个良好的大小,如4K或16K,并坚持使用它。如果你确信这是你的应用程序中的瓶颈,那么你应该开始进行分析以找到最佳缓冲区大小。如果你选择的大小太小,你将浪费时间做额外的I/O操作和额外的函数调用。如果你选择的大小过大,你会开始看到很多缓存未命中,这会使你变得非常慢。不要使用比你的L2缓存大小更大的缓冲区。

5
在 BufferedInputStream 的源代码中,您会发现:private static int DEFAULT_BUFFER_SIZE = 8192; 因此,您可以使用该默认值。但是,如果您能够找到更多信息,您将获得更有价值的答案。例如,您的 ADSL 可能需要一个缓冲区大小为 1454 字节,这是因为 TCP/IP 的有效负载。对于磁盘,您可能需要使用与磁盘块大小匹配的值。

5
在理想情况下,我们应该有足够的内存来进行一次读取操作。这将是最佳性能,因为我们让系统自由管理文件系统、分配单元和硬盘驱动器。 实际上,你很幸运能提前知道文件大小,只需使用平均文件大小向上取整到4K(NTFS上的默认分配单元)即可。 最好的方法是:创建一个基准测试来测试多个选项。

你的意思是文件读写的最佳缓冲区大小是4k吗? - BattleTested_закалённый в бою

5
你可以使用BufferedStreams/readers,并使用它们的缓冲区大小。
我相信BufferedXStreams使用8192作为缓冲区大小,但像Ovidiu所说,你应该在许多选项上运行测试。最好的大小取决于文件系统和磁盘配置。

5

使用Java NIO的FileChannel和MappedByteBuffer读取文件很可能会比使用FileInputStream更快。基本上,将大文件内存映射,对于小文件使用直接缓冲区。


2

如其他答案中所述,请使用BufferedInputStreams。

之后,我猜缓冲区大小并不重要。无论程序是I/O绑定的,还是通过增加缓冲区大小超过BIS默认值来提高性能,都不会产生太大影响。

或者程序在MessageDigest.update()内部被CPU绑定,并且大部分时间都没有花费在应用程序代码上,因此调整它也没有帮助。

(嗯...有了多核心,线程可能会有帮助。)


0

1024适用于各种情况,尽管在实践中,您可能会发现使用更大或更小的缓冲区大小可以获得更好的性能。

这取决于许多因素,包括文件系统块大小和CPU硬件。

选择2的幂作为缓冲区大小也很常见,因为大多数底层硬件都是以2的幂结构化的,包括文件块和缓存大小。Buffered类允许您在构造函数中指定缓冲区大小。如果没有提供,则它们将使用默认值,在大多数JVM中,默认值是2的幂。

无论您选择哪种缓冲区大小,从非缓冲到缓冲文件访问的最大性能提升都将是显著的。调整缓冲区大小可能会稍微提高性能,但除非您使用的是极小或极大的缓冲区大小,否则不太可能产生重大影响。



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