Основной причиной того, что нативные инструменты могут обрабатывать такие текстовые файлы намного быстрее, является их предположение об одном конкретном наборе символов, особенно когда он имеет 8-битовое кодирование на основе ASCII, тогда как Java выполняет преобразование байтов в символы, абстракция которого способна поддерживать произвольные кодировки.
Когда мы аналогичным образом предполагаем наличие одного набора символов со свойствами, указанными выше, мы можем использовать инструменты низкого уровня, которые могут значительно повысить производительность.
Для такой операции мы определяем следующие вспомогательные методы:
private static char[] getTable(Charset cs) {
if(cs.newEncoder().maxBytesPerChar() != 1f)
throw new UnsupportedOperationException("Not an 8 bit charset");
byte[] raw = new byte[256];
IntStream.range(0, 256).forEach(i -> raw[i] = (byte)i);
char[] table = new char[256];
cs.newDecoder().onUnmappableCharacter(CodingErrorAction.REPLACE)
.decode(ByteBuffer.wrap(raw), CharBuffer.wrap(table), true);
for(int i = 0; i < 128; i++)
if(table[i] != i) throw new UnsupportedOperationException("Not ASCII based");
return table;
}
и
private static CharSequence mapAsciiBasedText(Path p, char[] table) throws IOException {
try(FileChannel fch = FileChannel.open(p, StandardOpenOption.READ)) {
long actualSize = fch.size();
int size = (int)actualSize;
if(size != actualSize) throw new UnsupportedOperationException("file too large");
MappedByteBuffer mbb = fch.map(FileChannel.MapMode.READ_ONLY, 0, actualSize);
final class MappedCharSequence implements CharSequence {
final int start, size;
MappedCharSequence(int start, int size) {
this.start = start;
this.size = size;
}
public int length() {
return size;
}
public char charAt(int index) {
if(index < 0 || index >= size) throw new IndexOutOfBoundsException();
byte b = mbb.get(start + index);
return b<0? table[b+256]: (char)b;
}
public CharSequence subSequence(int start, int end) {
int newSize = end - start;
if(start<0 || end < start || end-start > size)
throw new IndexOutOfBoundsException();
return new MappedCharSequence(start + this.start, newSize);
}
public String toString() {
return new StringBuilder(size).append(this).toString();
}
}
return new MappedCharSequence(0, size);
}
}
Это позволяет отобразить файл в виртуальную память и проецировать его непосредственно на CharSequence
, без операций копирования, предполагая, что сопоставление может быть выполнено с помощью простой таблицы и, для кодировок на основе ASCII, большинства символов даже не нужно искать таблицы, так как их числовое значение идентично коду Unicode.
С помощью этих методов вы можете реализовать операцию как
// You need this only once per JVM.
// Note that running inside IDEs like Netbeans may change the default encoding
char[] table = getTable(Charset.defaultCharset());
try(Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))
.filter(Files::isRegularFile)) {
Pattern pattern = Pattern.compile(String.join("|", Strings_PASSWORDS));
long startTime = System.nanoTime();
final List<Path> filesWithObviousStringss = stream//.parallel()
.filter(path -> {
try {
return pattern.matcher(mapAsciiBasedText(path, table)).find();
} catch(IOException ex) {
throw new UncheckedIOException(ex);
}
})
.collect(Collectors.toList());
System.out.println("Time taken = "
+ TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()-startTime) + " seconds");
}
Это выполняется намного быстрее, чем обычное преобразование текста, но все еще поддерживает параллельное выполнение.
Помимо необходимости использования однобайтовой кодировки на основе ASCII, существует ограничение, заключающееся в том, что этот код не поддерживает файлы размером более 2 ГиБ. Хотя это решение можно расширить для поддержки файлов большего размера, я бы не стал добавлять это усложнение, если оно действительно не требуется.