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

时间:2009-11-16 11:17:03

标签: java optimization hash performance sha256

我需要计算一个大文件(或其中一部分)的SHA-256哈希值。我的实现工作正常,但它比C ++的CryptoPP计算慢得多(25分钟与10分钟~30GB文件)。我需要的是在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 个答案:

答案 0 :(得分:34)

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

BTW:您可以通过在FileInputStream周围包装DigestInputStream,读取文件并从DigestInputStream获取计算的哈希,而不是像在代码中那样手动将数据从RandomAccessFile拖放到MessageDigest来简化您的实现。< / p>

我使用较旧的Java版本进行了一些性能测试,这里似乎存在Java 5和Java 6之间的相关差异。我不确定SHA实现是否已经优化,或者VM是否正在以更快的速度执行代码。我使用不同的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实现中的影响有点好奇,因为benchmarks results表明SHA-256算法在Opteron上只需要15.8个CPU周期/字节。遗憾的是,我无法在cygwin上使用gcc构建CryptoPP(构建成功,但生成的exe立即失败),但在CryptoPP中使用和不使用汇编程序支持并使用VS SHA构建VS2005(默认发行版配置)的性能基准测试在内存缓冲区上实现,省略任何磁盘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,并且只获得了appr。与VS exe的结果相比,吞吐量的一半。我不确定虚拟机有多大的差异以及VS通常会产生比gcc更好的代码导致多少差异,但我现在无法从gcc获得更精确的结果。

答案 1 :(得分:4)

也许今天的第一件事就是找出你花费最多时间的地方?您可以通过分析器运行它,看看花费的时间最多。

可能的改进:

  1. 使用NIO读取fastest possible way
  2. 中的文件
  3. 在单独的线程中更新哈希。这实际上很难做到,并不适合胆小的人,因为它涉及线程之间的安全发布。但是,如果您的分析显示在哈希算法中花费了大量时间,则可以更好地利用磁盘。

答案 2 :(得分:2)

我建议您使用像JProfiler这样的分析器或集成在Netbeans中的分析器(免费)来查找实际花费的时间并专注于该部分。

只是一个疯狂的猜测 - 不确定它是否会有所帮助 - 但你尝试过服务器虚拟机吗?尝试使用java -server启动应用,看看是否对您有所帮助。与默认客户端VM相比,服务器VM更积极地将Java代码编译为本机。

答案 3 :(得分:1)

以前,Java比同一个C ++代码慢了大约10倍。如今速度接近2倍。我认为你遇到的只是Java的一个基本部分。 JVM会变得更快,特别是在发现新的JIT技术时,你将很难执行C。

您是否尝试过其他JVM和/或编译器?我曾经使用JRocket获得更好的性能,但稳定性较差。同样使用jikes而不是javac。

答案 4 :(得分:1)

由于你显然有一个很快的工作C ++实现,你可以构建一个JNI桥并使用实际的C ++实现,或者你可以尝试不重新发明轮子,特别是因为它是一个很大的并使用一个预制库,例如BouncyCastle,用于解决程序的所有加密需求。

答案 5 :(得分:1)

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

答案 6 :(得分:0)

你的代码如此之慢的主要原因是因为你使用的RandomAccessFile在性能方面总是很慢。我建议使用“BufferedInputStream”,以便您可以从磁盘操作系统的操作系统级缓存的所有功能中受益。

代码应该类似于:

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