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

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

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

顺便说一下:你可以通过在FileInputStream周围包装DigestInputStream来大量简化你的实现,读取文件并从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立即失败),但在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. 使用NIO以最快的方式读取文件
  2. 在单独的线程中更新哈希。 这实际上很难做到,并不适合胆小者,因为它涉及线程之间的安全发布。 但是,如果您的分析显示在哈希算法中花费了大量时间,则可以更好地使用磁盘。

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

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

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

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

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

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

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

代码应该类似于:

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