FileChannel ByteBuffer и Hashing Files
Я построил метод хэширования файла в java, который принимает входное представление строки filepath+filename
, а затем вычисляет хэш этого файла. Хеш может быть любым из встроенного поддерживающего java хэширования algo, такого как MD2
через SHA-512
.
Я пытаюсь выполнить каждую последнюю производительность, так как этот метод является неотъемлемой частью проекта, над которым я работаю. Мне рекомендуется использовать FileChannel
вместо обычного FileInputStream
.
Мой оригинальный метод:
/**
* Gets Hash of file.
*
* @param file String path + filename of file to get hash.
* @param hashAlgo Hash algorithm to use. <br/>
* Supported algorithms are: <br/>
* MD2, MD5 <br/>
* SHA-1 <br/>
* SHA-256, SHA-384, SHA-512
* @return String value of hash. (Variable length dependent on hash algorithm used)
* @throws IOException If file is invalid.
* @throws HashTypeException If no supported or valid hash algorithm was found.
*/
public String getHash(String file, String hashAlgo) throws IOException, HashTypeException {
StringBuffer hexString = null;
try {
MessageDigest md = MessageDigest.getInstance(validateHashType(hashAlgo));
FileInputStream fis = new FileInputStream(file);
byte[] dataBytes = new byte[1024];
int nread = 0;
while ((nread = fis.read(dataBytes)) != -1) {
md.update(dataBytes, 0, nread);
}
fis.close();
byte[] mdbytes = md.digest();
hexString = new StringBuffer();
for (int i = 0; i < mdbytes.length; i++) {
hexString.append(Integer.toHexString((0xFF & mdbytes[i])));
}
return hexString.toString();
} catch (NoSuchAlgorithmException | HashTypeException e) {
throw new HashTypeException("Unsuppored Hash Algorithm.", e);
}
}
Рефакторированный метод:
/**
* Gets Hash of file.
*
* @param file String path + filename of file to get hash.
* @param hashAlgo Hash algorithm to use. <br/>
* Supported algorithms are: <br/>
* MD2, MD5 <br/>
* SHA-1 <br/>
* SHA-256, SHA-384, SHA-512
* @return String value of hash. (Variable length dependent on hash algorithm used)
* @throws IOException If file is invalid.
* @throws HashTypeException If no supported or valid hash algorithm was found.
*/
public String getHash(String fileStr, String hashAlgo) throws IOException, HasherException {
File file = new File(fileStr);
MessageDigest md = null;
FileInputStream fis = null;
FileChannel fc = null;
ByteBuffer bbf = null;
StringBuilder hexString = null;
try {
md = MessageDigest.getInstance(hashAlgo);
fis = new FileInputStream(file);
fc = fis.getChannel();
bbf = ByteBuffer.allocate(1024); // allocation in bytes
int bytes;
while ((bytes = fc.read(bbf)) != -1) {
md.update(bbf.array(), 0, bytes);
}
fc.close();
fis.close();
byte[] mdbytes = md.digest();
hexString = new StringBuilder();
for (int i = 0; i < mdbytes.length; i++) {
hexString.append(Integer.toHexString((0xFF & mdbytes[i])));
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new HasherException("Unsupported Hash Algorithm.", e);
}
}
Оба возвращают правильный хеш, однако метод рефакторинга, похоже, только взаимодействует с небольшими файлами. Когда я перехожу в большой файл, он полностью задыхается, и я не могу понять, почему. Я новичок в NIO
, поэтому, пожалуйста, советую.
EDIT: Вспомните, что я бросаю SHA-512 через него для тестирования.
UPDATE:
Обновление с помощью моего текущего метода.
/**
* Gets Hash of file.
*
* @param file String path + filename of file to get hash.
* @param hashAlgo Hash algorithm to use. <br/>
* Supported algorithms are: <br/>
* MD2, MD5 <br/>
* SHA-1 <br/>
* SHA-256, SHA-384, SHA-512
* @return String value of hash. (Variable length dependent on hash algorithm used)
* @throws IOException If file is invalid.
* @throws HashTypeException If no supported or valid hash algorithm was found.
*/
public String getHash(String fileStr, String hashAlgo) throws IOException, HasherException {
File file = new File(fileStr);
MessageDigest md = null;
FileInputStream fis = null;
FileChannel fc = null;
ByteBuffer bbf = null;
StringBuilder hexString = null;
try {
md = MessageDigest.getInstance(hashAlgo);
fis = new FileInputStream(file);
fc = fis.getChannel();
bbf = ByteBuffer.allocateDirect(8192); // allocation in bytes - 1024, 2048, 4096, 8192
int b;
b = fc.read(bbf);
while ((b != -1) && (b != 0)) {
bbf.flip();
byte[] bytes = new byte[b];
bbf.get(bytes);
md.update(bytes, 0, b);
bbf.clear();
b = fc.read(bbf);
}
fis.close();
byte[] mdbytes = md.digest();
hexString = new StringBuilder();
for (int i = 0; i < mdbytes.length; i++) {
hexString.append(Integer.toHexString((0xFF & mdbytes[i])));
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new HasherException("Unsupported Hash Algorithm.", e);
}
}
Итак, я попытался установить хэширование MD5 файла 2.92GB, используя мой оригинальный пример и мой последний пример обновления. Конечно, любой сравнительный тест является относительным, поскольку происходит кеширование ОС и диска и другое "волшебство", которое будет искажать повторяющиеся чтения одних и тех же файлов... но здесь можно сделать несколько тестов. Я загрузил каждый метод и уволил его 5 раз после его компиляции. Тест был сделан из последнего (5-го) запуска, так как это был бы "самый горячий" запуск для этого алгоритма и любой "магии" (в моей теории так или иначе).
Here the benchmarks so far:
Original Method - 14.987909 (s)
Latest Method - 11.236802 (s)
Это 25.03% decrease
во времени, взятом для хэша того же файла 2.92 ГБ. Довольно хорошо.
Ответы
Ответ 1
3 предложения:
1) очистить буфер после каждого считывания
while (fc.read(bbf) != -1) {
md.update(bbf.array(), 0, bytes);
bbf.clear();
}
2) не закрывают как fc, так и fis, это избыточно, достаточно закрыть fis. API FileInputStream.close говорит:
If this stream has an associated channel then the channel is closed as well.
3), если вы хотите улучшить производительность с помощью FileChannel, используйте
ByteBuffer.allocateDirect(1024);
Ответ 2
Еще одно возможное улучшение может произойти, если код только назначил временный буфер один раз.
например.
int bufsize = 8192;
ByteBuffer buffer = ByteBuffer.allocateDirect(bufsize);
byte[] temp = new byte[bufsize];
int b = channel.read(buffer);
while (b > 0) {
buffer.flip();
buffer.get(temp, 0, b);
md.update(temp, 0, b);
buffer.clear();
b = channel.read(buffer);
}
Добавление
Примечание. В коде строкового кода есть ошибка. Он печатает ноль как одноразрядное число. Это можно легко устранить. например.
hexString.append(mdbytes[i] == 0 ? "00" : Integer.toHexString((0xFF & mdbytes[i])));
Кроме того, в качестве эксперимента я переписал код для использования отображенных байт-буферов. Он работает примерно на 30% быстрее (6-7 миллисов против 9-11 миллинов FWIW). Я ожидаю, что вы сможете получить больше от него, если вы написали код хеширования кода, который работал непосредственно в буфере байтов.
Я попытался объяснить инициализацию JVM и кеширование файловой системы путем хэширования другого файла с каждым алгоритмом перед запуском таймера. Первый пробег кода примерно в 25 раз медленнее обычного. По-видимому, это связано с инициализацией JVM, поскольку все прогоны в цикле синхронизации примерно одинаковой длины. Похоже, что они не используют кеширование. Я тестировал алгоритм MD5. Кроме того, во время временной части выполняется только один алгоритм на протяжении всей тестовой программы.
Код в цикле короче, поэтому потенциально более понятно. Я не на 100% уверен, что какое-то давление памяти, отображающее многие файлы под большим объемом, будет воздействовать на JVM, так что это может быть что-то, что вам нужно будет исследовать и подумать, хотите ли вы рассмотреть такое решение, если хотите запустить это под нагрузкой.
public static byte[] hash(File file, String hashAlgo) throws IOException {
FileInputStream inputStream = null;
try {
MessageDigest md = MessageDigest.getInstance(hashAlgo);
inputStream = new FileInputStream(file);
FileChannel channel = inputStream.getChannel();
long length = file.length();
if(length > Integer.MAX_VALUE) {
// you could make this work with some care,
// but this code does not bother.
throw new IOException("File "+file.getAbsolutePath()+" is too large.");
}
ByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, length);
int bufsize = 1024 * 8;
byte[] temp = new byte[bufsize];
int bytesRead = 0;
while (bytesRead < length) {
int numBytes = (int)length - bytesRead >= bufsize ?
bufsize :
(int)length - bytesRead;
buffer.get(temp, 0, numBytes);
md.update(temp, 0, numBytes);
bytesRead += numBytes;
}
byte[] mdbytes = md.digest();
return mdbytes;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Unsupported Hash Algorithm.", e);
}
finally {
if(inputStream != null) {
inputStream.close();
}
}
}
Ответ 3
Вот пример файлового хеширования с использованием NIO
- Путь
- FileChanngel
- MappedByteBuffer
И избегайте использования байта []. Таким образом, я думаю, должна улучшенная версия выше.
И второй пример nio, где хешированное значение сохраняется в пользовательских атрибутах. Что
может использоваться для генерации HTML etag других образцов там файл не изменяется.
public static final byte[] getFileHash(final File src, final String hashAlgo) throws IOException, NoSuchAlgorithmException {
final int BUFFER = 32 * 1024;
final Path file = src.toPath();
try(final FileChannel fc = FileChannel.open(file)) {
final long size = fc.size();
final MessageDigest hash = MessageDigest.getInstance(hashAlgo);
long position = 0;
while(position < size) {
final MappedByteBuffer data = fc.map(FileChannel.MapMode.READ_ONLY, 0, Math.min(size, BUFFER));
if(!data.isLoaded()) data.load();
System.out.println("POS:"+position);
hash.update(data);
position += data.limit();
if(position >= size) break;
}
return hash.digest();
}
}
public static final byte[] getCachedFileHash(final File src, final String hashAlgo) throws NoSuchAlgorithmException, FileNotFoundException, IOException{
final Path path = src.toPath();
if(!Files.isReadable(path)) return null;
final UserDefinedFileAttributeView view = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
final String name = "user.hash."+hashAlgo;
final ByteBuffer bb = ByteBuffer.allocate(64);
try { view.read(name, bb); return ((ByteBuffer)bb.flip()).array();
} catch(final NoSuchFileException t) { // Not yet calculated
} catch(final Throwable t) { t.printStackTrace(); }
System.out.println("Hash not found calculation");
final byte[] hash = getFileHash(src, hashAlgo);
view.write(name, ByteBuffer.wrap(hash));
return hash;
}