16

大きなファイル(またはその一部)のSHA-256ハッシュを計算する必要があります。私の実装は正常に機能しますが、C ++のCryptoPP計算よりもはるかに低速です(25分対約30GBファイルの10分)。私が必要としているのは、C ++とJavaでの同様の実行時間であるため、ハッシュはほぼ同時に準備ができています。バウンシーキャッスルの実装も試しましたが、同じ結果が得られました。ハッシュの計算方法は次のとおりです。

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) で読み込んでいることが原因です。システムのバッファ サイズ (バフ) を 16kB ではなく 1MB に増やすと、スループットが 2 倍以上になりますが、50MB/s を超えると、ディスク速度によって制限され、単一の CPU コアを完全にロードできません。

ところで: コードのように RandomAccessFile から MessageDigest にデータを手動でシャッフルする代わりに、DigestInputStream を FileInputStream にラップし、ファイルを読み込んで DigestInputStream から計算されたハッシュを取得することで、実装を大幅に簡素化できます。


以前の Java バージョンでいくつかのパフォーマンス テストを行ったところ、Java 5 と Java 6 の間に関連する違いがあるようです。ただし、SHA の実装が最適化されているのか、VM がコードをより高速に実行しているのかはわかりません。さまざまな Java バージョン (1MB バッファー) で得られるスループットは次のとおりです。

  • Sun JDK 1.5.0_15 (クライアント): 28MB/秒、CPU によって制限されます
  • Sun JDK 1.5.0_15 (サーバー): 45MB/秒、CPU によって制限されます
  • Sun JDK 1.6.0_16 (クライアント): 42MB/秒、CPU によって制限されます
  • Sun JDK 1.6.0_16 (サーバー): 52MB/秒、ディスク I/O によって制限される (85-90% の CPU 負荷)

ベンチマークの結果は、SHA-256 アルゴリズムが Opteron で 15.8 CPU サイクル/バイトしか必要としないことを示しているため、CryptoPP SHA 実装におけるアセンブラー部分の影響に少し興味がありました。残念ながら、cygwin で gcc を使用して CryptoPP をビルドすることはできませんでした (ビルドは成功しましたが、生成された exe はすぐに失敗しました) が、CryptoPP でのアセンブラー サポートの有無にかかわらず、VS2005 (デフォルトのリリース構成) でパフォーマンス ベンチマークを構築し、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 をビルドし、ベンチマークすることができました。VS exe の結果と比較してスループットが半分になります。どの程度の違いが仮想マシンに寄与しているのか、VS が通常 gcc よりも優れたコードを生成していることに起因するのかはわかりませんが、今のところ gcc からより正確な結果を得る方法はありません。

于 2009-11-16T14:07:28.013 に答える
4

おそらく、今日の最初のことは、最も多くの時間を費やしている場所で運動することでしょうか? プロファイラーで実行して、最も時間が費やされている場所を確認できますか?

考えられる改善:

  1. NIO を使用して、可能な限り高速な方法でファイルを読み取ります
  2. 別のスレッドでハッシュを更新します。これは実際にはかなり難しく、スレッド間で安全に公開する必要があるため、気弱な人向けではありません。ただし、プロファイリングでハッシュ アルゴリズムにかなりの時間が費やされていることが示されている場合は、ディスクをより有効に活用できる可能性があります。
于 2009-11-16T11:33:53.750 に答える
2

JProfiler や Netbeans に統合されているプロファイラー (無料) を使用して、実際に時間が費やされている場所を見つけ、その部分に集中することをお勧めします。

勝手な推測ですが、役に立つかどうかはわかりませんが、Server VM を試してみましたか? でアプリを起動してみてjava -server、それが役立つかどうかを確認してください。サーバー VM は、デフォルトのクライアント VM よりも積極的に Java コードをネイティブにコンパイルします。

于 2009-11-16T11:36:16.773 に答える
1

以前は、Java は同じ C++ コードよりも約 10 倍遅く実行されていました。最近は 2 倍近く遅くなります。あなたが遭遇したことは、Java の基本的な部分にすぎないと思います。特に新しい JIT 手法が発見されると、JVM は高速化されますが、C を実行するのに苦労するでしょう。

代替の JVM やコンパイラを試しましたか? 以前はJRocketを使用するとパフォーマンスが向上しましたが、安定性は低下しました。javac でjikesを使用する場合も同様です。

于 2009-11-16T11:32:29.247 に答える
1

どうやら高速な動作中の C++ 実装があるので、JNIブリッジを構築して実際の C++ 実装を使用するか、車輪を再発明しないで試すことができます。プログラムのすべての暗号化ニーズを解決するために作成されました。

于 2009-11-16T11:41:53.767 に答える
1

このパフォーマンスの違いは、プラットフォームにのみ関連していると思います。バッファ サイズを変更してみて、改善があるかどうかを確認してください。そうでない場合は、JNI (Java Native Interface)を使用します。Java から C++ 実装を呼び出すだけです。

于 2009-11-16T11:44:33.547 に答える
0

コードが非常に遅い主な理由は、パフォーマンス的に常に非常に遅いRandomAccessFileを使用しているためです。disk-i / oのOSレベルのキャッシュのすべての機能を活用できるように、「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;
}
于 2010-02-06T14:04:18.847 に答える