2016-12-25 30 views
38

Tôi cần phải đọc từng tệp một ký tự và tôi đang sử dụng phương pháp read() từ BufferedReader. *Tại sao BufferedReader đọc() chậm hơn nhiều so với readLine()?

Tôi thấy rằng read() là khoảng 10x chậm hơn readLine(). Đây có phải là dự kiến ​​không? Hay tôi đang làm gì sai?

Dưới đây là một chuẩn mực với Java 7. Hồ sơ kiểm tra đầu vào có khoảng 5 triệu dòng và 254 triệu ký tự (~ 242 MB) **:

Phương pháp read() mất khoảng 7000 ms để đọc tất cả các nhân vật:

@Test 
public void testRead() throws IOException, UnindexableFastaFileException{ 

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa"))); 

    long t0= System.currentTimeMillis(); 
    int c; 
    while((c = fa.read()) != -1){ 
     // 
    } 
    long t1= System.currentTimeMillis(); 
    System.err.println(t1-t0); // ~ 7000 ms 

} 

phương pháp readLine() chỉ mất ~ 700 ms:

@Test 
public void testReadLine() throws IOException{ 

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa"))); 

    String line; 
    long t0= System.currentTimeMillis(); 
    while((line = fa.readLine()) != null){ 
     // 
    } 
    long t1= System.currentTimeMillis(); 
    System.err.println(t1-t0); // ~ 700 ms 
} 

* Mục đích thực tế: Tôi cần biết chiều dài của mỗi dòng, bao gồm các ký tự dòng mới (\n hoặc \r\n) VÀ chiều dài dòng sau khi tước chúng. Tôi cũng cần biết liệu một dòng có bắt đầu bằng ký tự > hay không. Đối với một tệp nhất định, thao tác này chỉ được thực hiện một lần khi bắt đầu chương trình. Vì các ký tự EOL không được trả về bởi BufferedReader.readLine() Tôi đang sử dụng phương thức read(). Nếu có cách tốt hơn để làm điều này, xin vui lòng nói.

** Tệp được nén ở đây là http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz. Đối với những người có thể tự hỏi, tôi đang viết một lớp để lập chỉ mục các tệp fasta.

+11

Vui lòng đọc về cách viết các tiêu chuẩn Java chính xác. –

+6

@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

+1

@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. –

Trả lời

34

Đ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.

+0

readCharTest phải là 'readTest()'? (Tôi sẽ sớm xóa nhận xét này) – Marco13

+0

Tin vui! Tôi đã có thể tái tạo kết quả của bạn nhưng tôi nghĩ rằng tiếng ồn được giới thiệu làm cho chúng phần lớn không hợp lệ - bạn đang đo độ thanh toán bù trừ bộ nhớ cache và bạn đã thêm quá trình xử lý không tương đương áp đảo điều thực sự đo được. Tôi có hai phê bình chung - một là (một 'dựa trên ý kiến' nhiều hơn) là điều này không thực sự là một microbenchmark vì vậy phương pháp luận chính nó là không đại diện. Một điều khác là, ngay cả khi chúng tôi chấp nhận phương pháp luận, không khó để đưa ra sự khác biệt về hiệu suất giữa 50% và 300% - tức là các phép đo cụ thể này không mang tính đại diện. – pvg

+0

Tôi sẽ cố gắng viết kết quả của mình vào ngày mai và đăng chúng. – pvg

0

Thật không ngạc nhiên khi thấy sự khác biệt này nếu bạn nghĩ về nó. Một thử nghiệm đang lặp lại các dòng trong một tệp văn bản, trong khi thử nghiệm kia đang lặp lại các ký tự.

Trừ khi mỗi dòng chứa một ký tự, được mong đợi là readLine() là cách nhanh hơn phương pháp read(). (mặc dù như được chỉ ra bởi các nhận xét ở trên, có thể lập luận vì BufferedReader đệm đầu vào, trong khi đọc tệp vật lý có thể không phải là hoạt động duy nhất đang hoạt động)

Nếu bạn thực sự muốn kiểm tra sự khác biệt giữa 2 tôi sẽ đề xuất thiết lập nơi bạn lặp qua từng ký tự trong cả hai bài kiểm tra. Ví dụ. một cái gì đó như:

void readTest(BufferedReader r) 
{ 
    int c; 
    StringBuilder b = new StringBuilder(); 
    while((c = r.read()) != -1) 
     b.append((char)c); 
} 

void readLineTest(BufferedReader r) 
{ 
    String line; 
    StringBuilder b = new StringBuilder(); 
    while((line = b.readLine())!= null) 
     for(int i = 0; i< line.length; i++) 
      b.append(line.charAt(i)); 
} 

Bên cạnh đó, hãy sử dụng "công cụ chẩn đoán hiệu suất Java" để chuẩn mã của bạn. Ngoài ra, đọc trên how to microbenchmark java code.

+4

Đây không thực sự là một microbenchmark. Cách tiếp cận áp phích, tuy nhiên nguyên thủy, không phải là không hợp lý cho các tỷ lệ thời gian và thời gian liên quan. Bạn có thể sử dụng lệnh thời gian unix cho điều này với sự tự tin phong nha bạn đang thấy một hiệu ứng đáng kể. – pvg

-1

Java JIT tối ưu hóa đi các cơ quan vòng lặp trống, vì vậy vòng của bạn thực sự trông như thế này:

while((c = fa.read()) != -1); 

while((line = fa.readLine()) != null); 

tôi đề nghị bạn đọc lên trên điểm chuẩn here và tối ưu hóa các vòng here .


Là tại sao thời gian thực hiện khác đi:

  • Lý do một (Điều này chỉ áp dụng nếu các cơ quan của vòng chứa mã): Trong ví dụ đầu tiên, bạn đang làm một hoạt động trên mỗi dòng, trong lần thứ hai, bạn đang thực hiện một cho mỗi ký tự. Điều này làm tăng thêm số dòng/ký tự bạn có.

    while((c = fa.read()) != -1){ 
        //One operation per character. 
    } 
    
    while((line = fa.readLine()) != null){ 
        //One operation per line. 
    } 
    
  • Lý do hai: Trong lớp BufferedReader, phương pháp readLine() không sử dụng read() đằng sau hậu trường - nó sử dụng mã riêng của nó. Phương thức readLine() thực hiện ít thao tác trên mỗi ký tự để đọc một dòng, hơn là đọc một dòng với phương thức read() - đây là lý do tại sao readLine() đọc nhanh toàn bộ tệp.

  • Lý do ba: Cần lặp lại nhiều lần để đọc từng ký tự hơn là đọc từng dòng (trừ khi mỗi ký tự nằm trên một dòng mới); read() được gọi nhiều lần hơn readLine().

+2

Nếu java tối ưu hóa các vòng lặp này, sẽ không có sự khác biệt về thời gian. – pvg

+0

@pvg Vui lòng xem chỉnh sửa. 'read' và' readLine' đọc tệp khác nhau. Và họ vẫn đang được gọi trong vòng lặp. –

+0

Tôi không nghĩ rằng vòng lặp trống có vấn đề gì. Tôi đặt 'if (line.contains ("> ")) {System.out.println (dòng); } 'bên trong vòng lặp của bài kiểm tra readLine() và' if (c == '>') {System.out.println (c); }; 'bên trong read(). Kết quả vẫn giữ nguyên. – dariober

1

Cảm ơn @Voo vì đã sửa. Những gì tôi đã đề cập bên dưới là chính xác từ FileReader#read() v/s BufferedReader#readLine() điểm xem NHƯNG không chính xác từ BufferedReader#read() v/s BufferedReader#readLine() quan điểm, vì vậy tôi đã loại bỏ câu trả lời.

Sử dụng phương pháp read() trên BufferedReader không phải là một ý tưởng hay, nó sẽ không gây ra bất kỳ tác hại nào nhưng chắc chắn sẽ lãng phí mục đích của lớp học.

Toàn bộ mục đích trong cuộc sống BufferedReader là giảm i/o bằng cách đệm nội dung. Bạn có thể đọc here trong hướng dẫn Java. Bạn cũng có thể nhận thấy rằng phương pháp read() trong BufferedReader thực sự được kế thừa từ Reader trong khi readLine() là phương pháp riêng của BufferedReader.

Nếu bạn muốn sử dụng phương pháp read() thì tôi sẽ nói bạn sử dụng tốt hơn FileReader, có nghĩa là cho mục đích đó. Bạn có thể read ở đây trong hướng dẫn Java.

Vì vậy, Tôi nghĩ rằng câu trả lời cho câu hỏi của bạn là rất đơn giản (mà không đi vào băng ghế dự bị đánh dấu và tất cả những gì sự giải thích) -

  • Mỗi read() được xử lý bởi hệ điều hành cơ bản và gây nên truy cập đĩa, hoạt động mạng hoặc một số hoạt động khác tương đối đắt tiền.
  • Khi bạn sử dụng readLine() thì bạn lưu tất cả các chi phí này, vì vậy readLine() sẽ luôn nhanh hơn read(), có thể không đáng kể đối với dữ liệu nhỏ nhưng nhanh hơn.
+2

Như đã đề cập trong phần bình luận: Mục tiêu phía sau trình đọc 'Buffered' (!) Là nó * đệm * một số dữ liệu. Vì vậy, lặp đi lặp lại 'read()' cuộc gọi sẽ * không * gây ra các byte được đọc từ đĩa từng người một. Thay vào đó, nó thường xuyên đọc "khối" dữ liệu. Bạn thậm chí có thể theo dõi nó xuống để xem tha trong cả hai, 'read' và' readLine' phương pháp tiếp cận, bên dưới 'FileReader' đang làm các cuộc gọi' read' cùng, mỗi đọc 8192 byte. – Marco13

+0

@ Marco13 Có rất nhiều bình luận địa ngục trong bài viết này và tôi thậm chí không đọc một vài câu, tôi đã đọc câu trả lời. Nếu điểm của bạn là 'đọc' cũng làm một số đệm thì tôi không chắc chắn, tuy nhiên tôi không thể loại trừ ở đó có thể có một số tối ưu hóa, nhưng vẫn còn những điều cơ bản vẫn giữ nguyên về mục đích của các lớp' BufferedReader' và 'FileReader', và tại sao 'read' chậm hơn' readLine' - vì có nhiều i/o hơn. – hagrawal

+0

@hagrawal Bạn thực sự có thể loại trừ điều đó một cách vô cùng dễ dàng bằng cách chỉ xem đoạn đầu tiên của tài liệu (hoặc xem nhanh mã). Mặc dù tên một mình dường như là một giveaway chết - nếu một * Buffered * Reader không đệm lần đọc, những gì khác nó sẽ làm gì? – Voo

0

Vì vậy, đây là câu trả lời thực tế cho câu hỏi của riêng tôi: Không sử dụng BufferedReader.read() sử dụng FileChannel để thay thế. (Rõ ràng là tôi không trả lời TẠI SAO tôi đặt trong tiêu đề). Dưới đây là điểm chuẩn nhanh chóng và dơ bẩn, hy vọng người khác sẽ tìm thấy nó hữu ích:

@Test 
public void testFileChannel() throws IOException{ 

    FileChannel fileChannel = FileChannel.open(Paths.get("chr1.fa")); 
    long n= 0; 
    int noOfBytesRead = 0; 

    long t0= System.nanoTime(); 

    while(noOfBytesRead != -1){ 
     ByteBuffer buffer = ByteBuffer.allocate(10000); 
     noOfBytesRead = fileChannel.read(buffer); 
     buffer.flip(); 
     while (buffer.hasRemaining()) { 
      char x= (char)buffer.get(); 
      n++; 
     } 
    } 
    long t1= System.nanoTime(); 
    System.err.println((float)(t1-t0)/1e6); // ~ 250 ms 
    System.err.println("nchars: " + n); // 254235640 chars read 
} 

Với ~ 250 ms để đọc các char tập tin toàn bộ bởi char, chiến lược này là nhanh hơn đáng kể so với BufferedReader.readLine() (~ 700 ms), chứ chưa nói read() . Thêm các câu lệnh trong vòng lặp để kiểm tra x == '\n'x == '>' tạo ra sự khác biệt nhỏ. Đồng thời đặt StringBuilder để tạo lại đường không ảnh hưởng đến thời gian quá nhiều. Vì vậy, điều này là rất tốt cho tôi (ít nhất là cho bây giờ).

Nhờ @ Marco13 đã đề cập đến FileChannel.

0

Theo tài liệu:

Mỗi read() gọi phương thức thực hiện cuộc gọi hệ thống đắt tiền.

Mọi cuộc gọi phương thức vẫn thực hiện cuộc gọi hệ thống đắt tiền, tuy nhiên, để có nhiều byte cùng một lúc hơn, do đó có ít cuộc gọi hơn.

Tình huống tương tự xảy ra khi chúng tôi tạo cơ sở dữ liệu update lệnh cho mỗi bản ghi mà chúng tôi muốn cập nhật, so với cập nhật hàng loạt, nơi chúng tôi thực hiện một cuộc gọi cho tất cả các bản ghi.

Các vấn đề liên quan