16

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

7 回答 7

37

我的解释可能无法解决您的问题,因为它很大程度上取决于您的实际运行时环境,但是当我在我的系统上运行您的代码时,吞吐量受磁盘 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 算法在 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 获得更准确的结果。

于 2009-11-16T14:07:28.013 回答
4

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

可能的改进:

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

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

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

于 2009-11-16T11:36:16.773 回答
1

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

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

于 2009-11-16T11:32:29.247 回答
1

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

于 2009-11-16T11:41:53.767 回答
1

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

于 2009-11-16T11:44:33.547 回答
0

您的代码如此缓慢的主要原因是您使用的 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;
}
于 2010-02-06T14:04:18.847 回答