Java:高效计算大文件的SHA-256哈希值

17

我需要计算一个大文件(或其中一部分)的SHA-256哈希值。我的实现可以正常工作,但速度比C++的CryptoPP计算要慢得多(对于约30GB的文件,25分钟 vs. 10分钟)。我需要在C++和Java中获得类似的执行时间,以便哈希几乎同时准备好。我还尝试了Bouncy Castle实现,但结果相同。以下是我计算哈希的方法:

int buff = 16384;
try {
    RandomAccessFile file = new RandomAccessFile("T:\\someLargeFile.m2v", "r");

    long startTime = System.nanoTime();
    MessageDigest hashSum = MessageDigest.getInstance("SHA-256");

    byte[] buffer = new byte[buff];
    byte[] partialHash = null;

    long read = 0;

    // calculate the hash of the hole file for the test
    long offset = file.length();
    int unitsize;
    while (read < offset) {
        unitsize = (int) (((offset - read) >= buff) ? buff : (offset - read));
        file.read(buffer, 0, unitsize);

        hashSum.update(buffer, 0, unitsize);

        read += unitsize;
    }

    file.close();
    partialHash = new byte[hashSum.getDigestLength()];
    partialHash = hashSum.digest();

    long endTime = System.nanoTime();

    System.out.println(endTime - startTime);

} catch (FileNotFoundException e) {
    e.printStackTrace();
}
7个回答

37

我的解释可能无法解决你的问题,因为它在很大程度上取决于你实际的运行环境,但是当我在我的系统上运行你的代码时,吞吐量受到磁盘I/O而不是哈希计算的限制。问题并没有通过切换到NIO解决,而仅仅是由于你以非常小的片段(16KB)读取文件引起的。将我的系统上的缓冲区大小(buff)从16KB增加到1MB后,吞吐量增加了一倍以上,但是在每个CPU内核的负载达到50MB/s之后,我仍然受到磁盘速度的限制,无法完全加载单个CPU内核。

顺便说一句:你可以通过在FileInputStream周围包装DigestInputStream来简化你的实现,通过读取文件并从DigestInputStream中获取计算出的哈希值,而不是像你的代码中那样手动从RandomAccessFile中移动数据到MessageDigest中。


我对旧版Java进行了一些性能测试,Java 5和Java 6之间似乎存在显著差异。但我不确定SHA实现是否经过了优化,还是虚拟机执行代码的速度更快。不同Java版本(1MB缓冲区)的吞吐量如下:

  • Sun JDK 1.5.0_15(客户端):28MB/s,受CPU限制
  • Sun JDK 1.5.0_15(服务器):45MB/s,受CPU限制
  • Sun JDK 1.6.0_16(客户端):42MB/s,受CPU限制
  • Sun JDK 1.6.0_16(服务器):52MB/s,受磁盘I/O限制(85-90% CPU负载)

我对CryptoPP SHA实现中汇编部分的影响有些好奇,因为基准测试结果表明,SHA-256算法在Opteron上仅需要15.8个CPU周期/字节。不幸的是,我无法在cygwin上使用gcc构建CryptoPP(构建成功,但生成的exe立即失败),但在VS2005中构建了一个性能基准测试,并且在CrytoPP中启用或禁用汇编支持,与Java SHA实现在内存缓冲区上进行比较,排除任何磁盘I/O,我在2.5GHz Phenom上获得以下结果:

  • Sun JDK1.6.0_13(服务器):26.2个周期/字节
  • CryptoPP(仅限C++):21.8个周期/字节
  • CryptoPP(汇编):13.3个周期/字节

这两个基准测试都计算了一个4GB空字节数组的SHA哈希值,迭代处理块大小为1MB,并传递给MessageDigest#update(Java)或CryptoPP的SHA256.Update函数(C++)。

我能够在运行Linux的虚拟机中使用gcc 4.4.1(-O3)构建和基准CryptoPP,但吞吐量仅相当于VS exe的一半。我不确定差异的多少是贡献于虚拟机,还是由于VS通常比gcc生成更好的代码,但我目前无法从gcc中获得更准确的结果。


感谢您的结果!我在我的机器上进行了分析,显示计算哈希值所花费的时间很长,但增加缓冲区是一个好主意,执行时间为4GB的文件减少了约50秒。 - stefita
我必须承认,Java的性能仍然没有达到CryptoPP库所实现的吞吐量,但是查看CryptoPP源代码,SHA核心实际上是用汇编语言实现的。你不太可能通过Java程序实现相同的性能,但如果你增加缓冲区大小,使用服务器VM和/或升级到Java 6,性能也许已经足够了? - jarnbjo
请问您用什么工具来测量CPU周期? - Cuper Hector
每字节的循环次数是根据哈希4GB数据所需的实际时间简单计算得出的。 - jarnbjo
@jarnbjo,我非常喜欢你认真地深入细节的方式。太棒了! - Tyagi Akhilesh

4
也许今天首先需要做的事情是找出你最多时间花在哪里?你可以通过分析工具来运行它,看看最多时间花费在哪里。
可能的改进措施:
  1. 使用NIO以最快的方式读取文件。
  2. 在单独的线程中更新哈希值。这实际上很难做到,不适合胆小的人,因为它涉及线程之间的安全发布。但是,如果您的分析显示哈希算法花费了大量时间,那么它可能更好地利用磁盘。

2

我建议您使用像JProfiler或Netbeans中集成的分析器(免费)等分析工具,以找出实际花费时间的部分并专注于该部分。

这只是一个猜测 - 不确定它是否有用 - 但是您尝试过Server VM吗?请尝试使用java -server启动应用程序,看看是否有帮助。Server VM比默认的客户端VM更积极地将Java代码编译为本机代码。


关于-server的建议很好,但如果O.P.在64位平台上使用1.6 JVM,则可能是无意义的,因为-server是默认设置。 - Max A.
哇,这个参数刚刚让事情变得更快了。这是某种万能的解决方案吗? - Franklin
虽然不是万能的解决方案,但随着时间的推移,我已经学会了喜欢使用服务器VM来处理“客户端”应用程序。启动可能需要更长的时间,内存消耗可能会更高,但通常是值得的。在64位系统上,默认情况下会使用它,在32位系统上,如果安装了Java SDK(JRE仅带有客户端VM),则可以选择使用它。 - Daniel Schneller

1
你的代码运行缓慢的主要原因是使用了RandomAccessFile,这种方式在性能方面一直都比较慢。我建议使用“BufferedInputStream”,这样你就可以从操作系统级别的磁盘I/O缓存中受益。
代码应该类似于:
    public static byte [] hash(MessageDigest digest, BufferedInputStream in, int bufferSize) throws IOException {
    byte [] buffer = new byte[bufferSize];
    int sizeRead = -1;
    while ((sizeRead = in.read(buffer)) != -1) {
        digest.update(buffer, 0, sizeRead);
    }
    in.close();

    byte [] hash = null;
    hash = new byte[digest.getDigestLength()];
    hash = digest.digest();
    return hash;
}

1

以前Java运行速度大约比同样的C++代码慢10倍。现在差距缩小到2倍左右。我认为你遇到的只是Java的一个基本问题。随着新的JIT技术的发现,JVM将会变得更快,但是要想超越C语言还是很困难的。

你尝试过其他的JVM和编译器吗?我曾经使用JRocket获得更好的性能,但是稳定性较差。同样,使用jikes代替javac也是如此。


我更喜欢使用Sun JVM,因为大多数人已经安装了它,但我可能会尝试你提到的两个编译器。谢谢你的建议! - stefita

1

由于您似乎已经有一个快速的 C++ 实现,因此您可以构建 JNI 桥接并使用实际的 C++ 实现,或者您可以尝试不要重复造轮子,特别是因为它是一个大轮子,并使用预制库,例如 BouncyCastle,该库已被设计用于解决程序的所有加密需求。


正如我之前所写的,我也尝试使用BouncyCastle的SHA256Digest()实现,结果证明它几乎和之前的实现一样快。 - stefita
哦,不知怎么错过了,抱歉。 - Esko
不过你可能是对的,使用一个封装的C++函数可能是个好主意,因为所有的Java端改进似乎只会让速度降低1/5的时间。 - stefita

1

我认为这种性能差异可能只与平台有关。尝试更改缓冲区大小,看看是否有任何改进。如果没有,我会选择JNI(Java本地接口)。只需从Java调用C++实现即可。


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