【问题标题】:Java: Calculate SHA-256 hash of large file efficientlyJava:高效计算大文件的 SHA-256 哈希
【发布时间】:2010-12-17 00:38:07
【问题描述】:

我需要计算一个大文件(或其中的一部分)的 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();
}

【问题讨论】:

    标签: java optimization hash performance sha256


    【解决方案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;
    }
    

    【讨论】:

      【解决方案2】:

      我的解释可能无法解决您的问题,因为它很大程度上取决于您的实际运行时环境,但是当我在我的系统上运行您的代码时,吞吐量受到磁盘 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 实现中汇编程序部分的影响有点好奇,因为benchmarks results 表明 SHA-256 算法在 Opteron 上只需要 15.8 个 CPU 周期/字节。不幸的是,我无法在 cygwin 上使用 gcc 构建 CryptoPP(构建成功,但生成的 exe 立即失败),但是使用 VS2005(默认发布配置)构建性能基准测试,在 CryptoPP 中有和没有汇编程序支持,并与 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,并且只获得了 appr。与 VS exe 的结果相比,吞吐量减少了一半。我不确定有多少差异是由虚拟机造成的,又有多少是由 VS 通常产生比 gcc 更好的代码造成的,但我现在无法从 gcc 获得更准确的结果。

      【讨论】:

      • 感谢您的结果!我尝试在我的机器上对其进行分析,它显示在计算哈希上花费了大量时间,但增加缓冲区是个好主意,对于 4GB 文件,执行时间减少了大约 50 秒。
      • 我必须承认,Java 性能仍然不及 CryptoPP 库所达到的吞吐量,但查看 CryptoPP 源代码,SHA 内核实际上是在汇编程序中实现的。使用 Java 程序不太可能达到相同的性能,但如果增加缓冲区大小、使用服务器 VM 和/或升级到 Java 6,性能也许就足够了?
      • 请问您使用什么工具来测量 cpu 周期?
      • 周期数/字节数只是根据散列 4GB 数据所用的实际时间计算得出。
      • @jarnbjo 只是喜欢你努力深入细节的方式。太棒了!
      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

        • 正如我上面所写的,我还尝试使用 BouncyCastle 的实现 SHA256Digest(),结果证明速度几乎一样快。
        • 嗯,不知怎的错过了,抱歉。
        • np。但是你可能对使用包装的 C++ 函数是正确的,因为 Java 方面的所有改进似乎只将速度降低了 1/5。
        【解决方案5】:

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

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

        【讨论】:

        • 很好的建议:-server,但如果 O.P. 在 64 位平台上使用 1.6 JVM,则可能有争议,-server 是默认值。
        • 哇,这个参数让事情变得更好了。这是某种灵丹妙药吗?
        • 并不是真正的灵丹妙药,但随着时间的推移,我学会了喜欢服务器虚拟机,即使是“客户端”应用程序也是如此。启动可能需要更长的时间,内存消耗可能会更高,但这通常是值得的。在 64 位系统上您默认获得它,在 32 位系统上您可以选择,如果安装了 Java SDK(JRE 仅与客户端 VM 一起提供)。
        【解决方案6】:

        也许今天的第一件事就是弄清楚你在哪里花费的时间最多?您能否通过分析器运行它并查看花费最多的时间。

        可能的改进:

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

        【讨论】:

          【解决方案7】:

          过去,Java 的运行速度比相同的 C++ 代码慢大约 10 倍。现在的速度慢了近 2 倍。我认为您遇到的只是 Java 的一个基本部分。 JVM 会变得更快,尤其是在发现新的 JIT 技术时,但您将很难执行 C。

          您是否尝试过替代 JVM 和/或编译器?我曾经使用JRocket 获得更好的性能,但稳定性较差。在 javac 上使用 jikes 也是如此。

          【讨论】:

          • 我更喜欢保留 sun jvm,因为大多数人已经安装了它,但我可能会尝试你提到的两个编译器。感谢您的提示!
          猜你喜欢
          • 1970-01-01
          • 2014-08-19
          • 2023-03-27
          • 1970-01-01
          • 2019-07-21
          • 2023-03-14
          • 2012-06-15
          • 2023-03-18
          • 1970-01-01
          相关资源
          最近更新 更多