Java: высоко оценивать хэш SHA-256 большого файла

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

Ответы

Ответ 1

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

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


Я провел несколько тестов производительности со старыми версиями Java, и, похоже, здесь существует существенная разница между Java 5 и Java 6. Я не уверен, что если оптимизация SHA оптимизирована или VM быстрее выполняет код. Производительность, которую я получаю с различными версиями 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% загрузки процессора)

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

Причина MAIN, почему ваш код настолько медленный, заключается в том, что вы используете 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;
}