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

2022-09-02 23:19:10

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

答案 1

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

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


我用较旧的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实现中的影响有点好奇,因为基准测试结果表明,SHA-256算法只需要15.8个CPU周期/字节在Opteron上。不幸的是,我无法在cygwin上使用gcc构建CryptoPP(构建成功,但生成的exe立即失败),但是使用VS2005(默认发布配置)构建性能基准测试,在CryptoPP中具有和不具有汇编程序支持,并与内存中缓冲区上的Java SHA实现进行比较,省略任何磁盘I / O,我在2.5GHz Phenom上得到了以下结果:

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

这两个基准测试都计算4GB空字节数组的SHA哈希,以1MB的块迭代它,这些块被传递给MessageDigest#update(Java)或CryptoPP的SHA256。更新功能(C++)。

我能够在运行Linux的虚拟机中使用gcc 4.4.1(-O3)构建和基准测试CryptoPP,并且只得到了appr。吞吐量是 VS exe 结果的一半。我不确定有多少差异是由于虚拟机造成的,有多少是由VS通常产生比gcc更好的代码引起的,但是我现在没有办法从gcc获得更准确的结果。


答案 2

也许今天的第一件事就是找出你花最多时间的地方?您能否通过探查器运行它,并查看花费最多时间的位置。

可能的改进:

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

推荐