分析性能时,重要的是在开始之前拥有有效的基准。因此,让我们从一个简单的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
这是你所看到的问题。我能想到的最可能的原因是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,你可以做一些进一步的优化)进一步帮助提高性能。