Điều quan trọng khi phân tích hiệu suất là có điểm chuẩn hợp lệ trước khi bạn bắt đầu. Vì vậy, chúng ta hãy bắt đầu với một điểm chuẩn JMH đơn giản cho thấy hiệu suất mong đợi của chúng ta sau khi khởi động. Một điều chúng tôi phải xem xét là kể từ khi hệ điều hành hiện đại như dữ liệu tập tin cache được truy cập thường xuyên, chúng tôi cần một số cách để xóa cache giữa các bài kiểm tra. Trên Windows có một tiện ích nhỏ nhỏ that does just this - trên Linux, bạn có thể thực hiện nó bằng cách viết vào một số tệp giả ở đâu đó.
Mã này sau đó trông như sau:
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();
}
}
và nếu chúng ta chạy nó với
chcp 65001 # set codepage to utf-8
mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar
chúng tôi nhận được kết quả như sau (khoảng 2 giây là cần thiết để xóa bộ nhớ đệm cho tôi và tôi đang chạy cái này trên ổ cứng nên đó là lý do tại sao nó là một việc tốt hơn so với bạn):
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
Bất ngờ! Theo dự kiến, không có sự khác biệt về hiệu suất ở đây sau khi JVM đã ổn định ở chế độ ổn định. Nhưng có một ngoại lệ trong phương pháp readCharTest:
# Warmup Iteration 1: 6.186 s/op
# Warmup Iteration 2: 3.744 s/op
đây là vấn đề exaclty bạn đang thấy. Lý do rất có thể tôi nghĩ là OSR không hoạt động tốt ở đây hoặc JIT chỉ chạy quá muộn để tạo sự khác biệt trong lần lặp đầu tiên.
Tùy thuộc vào trường hợp sử dụng của bạn, đây có thể là một vấn đề lớn hoặc không đáng kể (nếu bạn đang đọc hàng nghìn tệp thì sẽ không thành vấn đề, nếu bạn chỉ đọc một vấn đề này).
Giải quyết vấn đề như vậy là không dễ dàng và không có giải pháp chung, mặc dù có nhiều cách để xử lý việc này.Một thử nghiệm dễ dàng để xem liệu chúng ta có đi đúng hướng hay không là chạy mã với tùy chọn -Xcomp
để buộc HotSpot biên dịch mọi phương thức trên lời gọi đầu tiên. Và thực sự làm như vậy, nguyên nhân sự chậm trễ lớn ở gọi đầu tiên để biến mất:
# Warmup Iteration 1: 3.965 s/op
# Warmup Iteration 2: 3.753 s/op
giải pháp có thể
Bây giờ chúng ta có một ý tưởng tốt vấn đề thực sự là gì (tôi đoán vẫn là tất cả những các giải pháp khá thẳng về phía trước và đơn giản: Giảm số lượng các cuộc gọi chức năng (vì vậy có, chúng tôi có thể đã đến giải pháp này mà không có mọi thứ ở trên, nhưng nó luôn luôn tốt đẹp để có một nắm vững vấn đề và có thể có một giải pháp không liên quan đến việc thay đổi nhiều mã).
Mã sau chạy liên tục nhanh hơn một trong hai loại kia - bạn có thể chơi với kích thước mảng nhưng đáng ngạc nhiên không quan trọng (có lẽ là trái ngược với các phương pháp khác read(char[])
không phải lấy khóa để chi phí cho mỗi cuộc gọi thấp hơn để bắt đầu với).
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;
}
này rất có thể là tốt đủ hiệu suất khôn ngoan, nhưng nếu bạn muốn cải thiện hiệu suất hơn nữa bằng cách sử dụng sức mạnh file mapping (sẽ không tính vào quá lớn sự cải thiện trong một trường hợp như thế này, nhưng nếu bạn biết rằng văn bản của bạn luôn là ASCII, bạn có thể thực hiện thêm một số tối ưu hóa nữa) giúp hiệu suất hơn nữa.
Vui lòng đọc về cách viết các tiêu chuẩn Java chính xác. –
@Louis Wasserman Phải thừa nhận rằng tôi không quan tâm quá nhiều về việc chính xác trong các tiêu chuẩn của mình. JUnit và 'currentTimeMillis()' không lý tưởng nhưng tôi nhận thấy rằng sự khác biệt thời gian 8-10x trên một tệp khá lớn là đủ lớn để đặt câu hỏi. – dariober
@dariober Bạn có thể sử dụng 'public int read (char [] cbuf, int off, int len) để đẩy IOException' thay vì trực tiếp sử dụng hàm' read' của bufferdreader. Cuối cùng, mục tiêu của bạn là tìm kết thúc của các dòng trong một tệp. Mặc dù tôi đã không tự kiểm tra nó, nhưng việc kiểm soát bộ đệm trong tay của bạn có lẽ sẽ cho bạn một kết quả tốt hơn. –