2014-10-22 2 views
3

내가 버퍼의 크기를 바꿀 때 BufferedReader에서 설명 할 수없는 이상한 결과가 나타납니다.BufferedReader에서 * 큰 * 버퍼로 성능이 저하되는 이유는 무엇입니까?

버퍼의 크기를 늘리면 성능이 점차 향상 될 것이고, 반환 설정은 상당히 빨리 줄어들 것이며 그 이후의 성능은 다소 평탄해질 것이라고 강력히 예상했습니다. 그러나 버퍼 크기가 매우 작 으면 버퍼 크기를 늘리면 느려질 것입니다..

여기에 최소한의 예가 나와 있습니다. 모든 작업은 텍스트 파일을 통해 실행되며 줄 길이의 합을 계산합니다.

public int traverseFile(int bufSize) throws IOException { 
    BufferedReader reader = new BufferedReader(new FileReader("words16"), bufSize*1024); 
    String line; 
    int total=0; 
    while ((line=reader.readLine())!=null) 
     total+=line.length(); 
    reader.close(); 
    return total; 
} 

다양한 버퍼 크기로 벤치마킹을 시도했는데 결과가 다소 이상합니다. 최대 약 256KB, 성능이 향상됩니다. 그 시점 이후에는 더 악화됩니다. 나는 그것이 버퍼를 할당하는 데 걸리는 단지 시간인지 궁금 그래서 나는 (아래 두 번째 줄 참조) 항상 같은 메모리 총량을 할당하기 위해 뭔가를 추가하는 시도 :

public int traverseFile(int bufSize) throws IOException { 
    byte[] pad = new byte[(65536-bufSize)*1024]; 
    BufferedReader reader = new BufferedReader(new FileReader("words16"), bufSize*1024); 
    String line; 
    int total=0; 
    while ((line=reader.readLine())!=null) 
     total+=line.length(); 
    reader.close(); 
    return total; 
} 

이 더 확률하지 않습니다. 두 개의 다른 컴퓨터에서 여전히 동일한 결과를 얻고 있습니다. 전체 결과는 다음과 같습니다.

Benchmark          Mode Samples Score Error Units 
j.t.BufferSizeBenchmark.traverse_test1_4K  avgt  100 363.987 ± 1.901 ms/op 
j.t.BufferSizeBenchmark.traverse_test2_16K  avgt  100 356.551 ± 0.330 ms/op 
j.t.BufferSizeBenchmark.traverse_test3_64K  avgt  100 353.462 ± 0.557 ms/op 
j.t.BufferSizeBenchmark.traverse_test4_256K  avgt  100 350.822 ± 0.562 ms/op 
j.t.BufferSizeBenchmark.traverse_test5_1024K  avgt  100 356.949 ± 0.338 ms/op 
j.t.BufferSizeBenchmark.traverse_test6_4096K  avgt  100 358.377 ± 0.388 ms/op 
j.t.BufferSizeBenchmark.traverse_test7_16384K avgt  100 367.890 ± 0.393 ms/op 
j.t.BufferSizeBenchmark.traverse_test8_65536K avgt  100 363.271 ± 0.228 ms/op 

자세히 알 수 있듯이 스위트 스폿은 약 256KB입니다. 차이는 크지 않지만 확실히 측정 가능합니다.

내가 생각할 수있는 것은 이것이 메모리 캐시와 관련이 있다는 것입니다. 그것은 쓰여지고있는 RAM이 읽히고있는 RAM에서 멀리 떨어져 있기 때문입니까? 그러나 그것이 순환 버퍼라면, 나는 그것이 사실이라는 것을 확신하지 못합니다 : 쓰여지고있는 것은 읽혀지고있는 것의 배후에있을 것입니다.

words16 파일은 80MB이므로 게시 할 수는 없지만 Fedora의 표준 /usr/share/dict/words 파일은 16 번 이상입니다. 필요한 경우 링크를 게시하는 방법을 찾을 수 있습니다. 내가 버퍼의 크기를 증가 할 때

@OutputTimeUnit(TimeUnit.MILLISECONDS) 
@BenchmarkMode(Mode.AverageTime) 
@OperationsPerInvocation(1) 
@Warmup(iterations = 30, time = 100, timeUnit = TimeUnit.MILLISECONDS) 
@Measurement(iterations = 100, time = 10000, timeUnit = TimeUnit.MILLISECONDS) 
@State(Scope.Thread) 
@Threads(1) 
@Fork(1) 
public class BufferSizeBenchmark { 

    public int traverseFile(int bufSize) throws IOException { 
     byte[] pad = new byte[(65536-bufSize)*1024]; 
     BufferedReader reader = new BufferedReader(new FileReader("words16"), bufSize*1024); 
     String line; 
     int total=0; 
     while ((line=reader.readLine())!=null) 
      total+=line.length(); 
     reader.close(); 
     return total; 
    } 

    @Benchmark 
    public int traverse_test1_4K() throws IOException { 
     return traverseFile(4); 
    } 

    @Benchmark 
    public int traverse_test2_16K() throws IOException { 
     return traverseFile(16); 
    } 

    @Benchmark 
    public int traverse_test3_64K() throws IOException { 
     return traverseFile(64); 
    } 

    @Benchmark 
    public int traverse_test4_256K() throws IOException { 
     return traverseFile(256); 
    } 

    @Benchmark 
    public int traverse_test5_1024K() throws IOException { 
     return traverseFile(1024); 
    } 

    @Benchmark 
    public int traverse_test6_4096K() throws IOException { 
     return traverseFile(4096); 
    } 

    @Benchmark 
    public int traverse_test7_16384K() throws IOException { 
     return traverseFile(16384); 
    } 

    @Benchmark 
    public int traverse_test8_65536K() throws IOException { 
     return traverseFile(65536); 
    } 

    public static void main(String[] args) throws RunnerException { 
     Options opt = new OptionsBuilder() 
       .include(
         ".*" + BufferSizeBenchmark.class.getSimpleName() + ".*") 
       .forks(1).build(); 

     new Runner(opt).run(); 
    } 

} 

가 왜 나쁜 성능을 얻고있다 :

여기 벤치마킹 코드입니까?

답변

0

이것은 캐시 라인 크기에 영향을 줄 가능성이 큽니다. 캐시는 너무 큰 버퍼를 사용하는 LRU 퇴거 정책을 사용하기 때문에 버퍼를 읽는 기회를 갖기 전에 버퍼의 "시작"에 작성한 내용이 축출됩니다.

0

256k는 일반적인 CPU 캐시 크기입니다! 테스트를 마친 CPU 유형은 무엇입니까?

그래서 어떻게됩니까? 256k 청크 이하를 읽으면 버퍼에 쓰여진 내용은 읽기가 액세스 할 때 여전히 CPU 캐시에 남아 있습니다. 청크의 크기가 256k보다 크면 읽은 마지막 256k가 CPU 캐시에 있으므로 처음부터 읽기가 시작되면 내용을 주 메모리에서 검색해야합니다.

두 번째 문제는 버퍼 할당입니다. 패딩 버퍼를 사용한 트릭은 영리하지만 할당 비용을 실제로 평균화하지는 않습니다. 그 이유는 할당의 실제 비용은 메모리를 예약하는 것이 아니라 메모리를 비우는 것입니다. 또한 OS는 실제 메모리에 처음 액세스 할 때까지 매핑을 연기 할 수 있습니다. 하지만 패딩 버퍼에 액세스하지 마십시오.

관련 문제