Java: Эффективно вычислить хэш большого файла SHA-256 - PullRequest
13 голосов
/ 16 ноября 2009

Мне нужно вычислить хэш SHA-256 для большого файла (или его части). Моя реализация работает нормально, но она намного медленнее, чем расчет CryptoPP в C ++ (25 минут против 10 минут для файла ~ 30 ГБ). Мне нужно примерно одинаковое время выполнения в 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();
}

Ответы [ 7 ]

34 голосов
/ 16 ноября 2009

Мое объяснение может не решить вашу проблему, так как оно во многом зависит от вашей реальной среды выполнения, но когда я запускаю ваш код в моей системе, пропускная способность ограничивается дисковым вводом / выводом, а не вычислением хеша. Проблема не решается переключением на NIO, а просто вызвана тем, что вы читаете файл очень маленькими кусочками (16 КБ). Увеличение размера буфера (buff) в моей системе до 1 МБ вместо 16 КБ более чем вдвое увеличивает пропускную способность, но при> 50 МБ / с я все еще ограничен скоростью диска и не могу полностью загрузить одно ядро ​​ЦП.

Кстати: вы можете значительно упростить вашу реализацию, обернув DigestInputStream вокруг FileInputStream, прочитав файл и получив вычисленный хеш из DigestInputStream вместо того, чтобы вручную перетасовывать данные из RandomAccessFile в MessageDigest, как в вашем коде.


Я провел несколько тестов производительности с более старыми версиями Java, и, похоже, здесь есть существенное различие между Java 5 и Java 6. Однако я не уверен, что реализация SHA оптимизирована или виртуальная машина выполняет код намного быстрее. Производительность, которую я получаю с разными версиями Java (буфер 1 МБ):

  • Sun JDK 1.5.0_15 (клиент): 28 МБ / с, ограничено ЦП
  • Sun JDK 1.5.0_15 (сервер): 45 МБ / с, ограничено ЦП
  • Sun JDK 1.6.0_16 (клиент): 42 МБ / с, ограничено ЦП
  • Sun JDK 1.6.0_16 (сервер): 52 МБ / с, ограничено дисковым вводом-выводом (загрузка ЦП 85-90%)

Мне было немного любопытно, как ассемблерная часть реализуется в реализации SHA CryptoPP, так как результаты теста производительности указывают, что алгоритму SHA-256 требуется только 15,8 циклов ЦП / байт на Opteron. Я, к сожалению, не смог собрать CryptoPP с gcc на cygwin (сборка завершилась успешно, но сгенерированный exe сразу же не удалось), но я построил тест производительности с VS2005 (конфигурация выпуска по умолчанию) с поддержкой ассемблера в CryptoPP и без нее и сравнивал с Java SHA Реализация на буфере в памяти, исключая любые дисковые операции ввода-вывода, я получаю следующие результаты на Phenom 2.5 ГГц:

  • Sun JDK1.6.0_13 (сервер): 26,2 такта / байт
  • CryptoPP (только C ++): 21,8 циклов / байт
  • CryptoPP (ассемблер): 13,3 циклов / байт

Оба теста вычисляют хэш SHA пустого байтового массива 4 ГБ, перебирая его по кусочкам по 1 МБ, которые передаются в MessageDigest # update (Java) или в функцию CryptoPP SHA256.Update (C ++).

Я смог собрать и протестировать CryptoPP с gcc 4.4.1 (-O3) на виртуальной машине под управлением Linux и получил только ок. половина пропускной способности по сравнению с результатами VS exe. Я не уверен, сколько различий вносит вклад в виртуальную машину и сколько вызвано тем, что VS обычно производит лучший код, чем gcc, но у меня нет способа получить более точные результаты от gcc прямо сейчас.

4 голосов
/ 16 ноября 2009

Возможно, первое, что сегодня происходит, это тренировка, на которой вы проводите больше всего времени? Можете ли вы запустить его через профилировщик и посмотреть, где больше всего времени тратится.

Возможные улучшения:

  1. Используйте NIO для чтения файла самым быстрым из возможных способов
  2. Обновление хэша в отдельном потоке. Это на самом деле довольно сложно сделать, и это не для слабонервных, поскольку это предполагает безопасную публикацию между потоками. Но если ваше профилирование показывает значительное время, затрачиваемое на алгоритм хэширования, оно может лучше использовать диск.
2 голосов
/ 16 ноября 2009

Я предлагаю вам использовать профилировщик, такой как JProfiler, или интегрированный в Netbeans (бесплатный), чтобы узнать, где на самом деле тратится время, и сконцентрироваться на этой части.

Просто дикая догадка - не уверен, поможет ли это, - но пробовали ли вы серверную виртуальную машину? Попробуйте запустить приложение с java -server и посмотрите, поможет ли это вам. Виртуальная машина сервера более агрессивна при компиляции Java-кода в нативный, чем клиентская виртуальная машина по умолчанию.

1 голос
/ 16 ноября 2009

Я думаю, что эта разница в производительности может быть связана только с платформой. Попробуйте изменить размер буфера и посмотрите, есть ли улучшения. Если нет, я бы выбрал JNI (собственный интерфейс Java) . Просто вызовите реализацию C ++ из Java.

1 голос
/ 16 ноября 2009

Поскольку у вас очевидно есть работающая реализация C ++, которая быстра, вы можете построить мост JNI и использовать реальную реализацию C ++, или, возможно, вы можете не изобретать колесо, тем более что оно большое и используйте готовую библиотеку, такую ​​как BouncyCastle , которая была создана для решения всех криптографических потребностей вашей программы.

1 голос
/ 16 ноября 2009

Раньше Java работала примерно в 10 раз медленнее, чем тот же код C ++. В настоящее время ближе к 2 раза медленнее. Я думаю, то, с чем вы сталкиваетесь, является просто фундаментальной частью Java. JVM будут работать быстрее, особенно если будут открыты новые методы JIT, но вам будет сложно выполнить C.

Вы пробовали альтернативные JVM и / или компиляторы? Раньше я получал более высокую производительность с JRocket , но с меньшей стабильностью. То же самое для использования jikes поверх javac.

0 голосов
/ 06 февраля 2010

ОСНОВНАЯ причина, почему ваш код такой медленный, заключается в том, что вы используете RandomAccessFile, который всегда был довольно медленным с точки зрения производительности. Я предлагаю использовать «BufferedInputStream», чтобы вы могли воспользоваться всеми возможностями кэширования на уровне ОС для disk-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;
}
...