Важное значение при анализе производительности - иметь действительный контрольный показатель перед началом работы. Итак, давайте начнем с простого теста JMH, который показывает, какова ожидаемая производительность после разминки.
С одной стороны, мы должны учитывать, что, поскольку современные операционные системы любят кэшировать данные файла, к которым регулярно обращаются, нам нужен способ очистки кэшей между тестами. В Windows есть небольшая небольшая утилита that does just this - на Linux вы сможете это сделать, написав где-нибудь какой-нибудь псевдофайл.
Затем код выглядит следующим образом:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Mode;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public class IoPerformanceBenchmark {
private static final String FILE_PATH = "test.fa";
@Benchmark
public int readTest() throws IOException, InterruptedException {
clearFileCaches();
int result = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
int value;
while ((value = reader.read()) != -1) {
result += value;
}
}
return result;
}
@Benchmark
public int readLineTest() throws IOException, InterruptedException {
clearFileCaches();
int result = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
String line;
while ((line = reader.readLine()) != null) {
result += line.chars().sum();
}
}
return result;
}
private void clearFileCaches() throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder("EmptyStandbyList.exe", "standbylist");
pb.inheritIO();
pb.start().waitFor();
}
}
и если мы запустим его с
chcp 65001 # set codepage to utf-8
mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar
мы получаем следующие результаты (около 2 секунд, необходимо очистить кэш-память для меня, и я бегу это на жесткий диск так, поэтому это хорошая сделка медленнее, чем для вас):
Benchmark Mode Cnt Score Error Units
IoPerformanceBenchmark.readLineTest avgt 20 3.749 ± 0.039 s/op
IoPerformanceBenchmark.readTest avgt 20 3.745 ± 0.023 s/op
Сюрприз! Как и ожидалось, здесь нет никакой разницы в производительности после того, как JVM заработала стабильный режим. Но есть один останец в методе readCharTest:
# Warmup Iteration 1: 6.186 s/op
# Warmup Iteration 2: 3.744 s/op
который exaclty проблема, что вы видите. Наиболее вероятная причина, по которой я могу думать, заключается в том, что OSR не делает здесь хорошую работу или что JIT работает слишком поздно, чтобы изменить ситуацию на первой итерации.
В зависимости от вашего варианта использования это может быть большой проблемой или незначительным (если вы читаете тысячу файлов, это не имеет значения, если вы только читаете это, это проблема).
Решение этой проблемы непросто и нет общих решений, хотя есть способы справиться с этим.Один простой тест, чтобы убедиться, что мы на правильном пути, - это запустить код с параметром -Xcomp
, который заставляет HotSpot компилировать каждый метод при первом вызове. И в самом деле делать это, вызывает большие задержки при первом вызове исчезнуть:
# Warmup Iteration 1: 3.965 s/op
# Warmup Iteration 2: 3.753 s/op
Возможное решение
Теперь, когда мы имеем хорошее представление о том, что фактическая проблема (я думаю, до сих пор все те, блокировки не объединены и не используют эффективную реализацию смещенных блокировок), решение довольно прямолинейно и просто: уменьшите количество вызовов функций (так что да, мы могли бы прийти к этому решению без всего выше, но всегда приятно иметь хорошо справиться с проблемой, и, возможно, было решение, которое не связано с изменением кода).
Следующий код работает последовательно быстрее, чем любой из двух других - вы можете играть с размером массива, но это удивительно неважно (по-видимому, потому что в отличие от других методов read(char[])
не нужно приобретать блокировку, поэтому стоимость за звонок ниже для начала).
private static final int BUFFER_SIZE = 256;
private char[] arr = new char[BUFFER_SIZE];
@Benchmark
public int readArrayTest() throws IOException, InterruptedException {
clearFileCaches();
int result = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
int charsRead;
while ((charsRead = reader.read(arr)) != -1) {
for (int i = 0; i < charsRead; i++) {
result += arr[i];
}
}
}
return result;
}
Это, скорее всего, достаточно хорошо производительности мудрая, но если вы хотите повысить производительность еще больше, используя file mapping мощи (не будет рассчитывать на слишком большое улучшение в случае, например, как это, но если вы знаете, что ваш текст всегда ASCII, вы можете сделать некоторые дальнейшие оптимизации), а также повысить производительность.
Прочитайте, как писать точные тесты Java. –
@Louis Wasserman По общему признанию, я не слишком заботился о том, чтобы быть точным в моих тестах. JUnit и 'currentTimeMillis()' не идеальны, но я решил, что разница в 8-10 раз в довольно большом файле достаточно велика, чтобы задать вопрос. – dariober
@ dariober Возможно, вам лучше использовать 'public int read (char [] cbuf, int off, int len) throws IOException' вместо прямого использования' read' функции bufferdreader. В конечном счете ваша цель - найти конец строк в файле. Хотя я сам его не тестировал, но контроль над буфером в руке, вероятно, даст вам лучший результат. –