Buffer 사이즈 따른 성능 영향 비교

File IO 성능 개선을 하기 위해 적정한 버퍼 사이즈를 찾는 것도 중요하다.

실행 결과

횟수
결과

1차

4K 평균 걸린 시간: 107.00 ms

8K 평균 걸린 시간: 68.40 ms

32K 평균 걸린 시간: 29.60 ms

100MB 평균 걸린 시간: 56.20 ms

2차

4K 평균 걸린 시간: 124.60 ms

8K 평균 걸린 시간: 58.40 ms

32K 평균 걸린 시간: 29.00 ms

100MB 평균 걸린 시간: 51.00 ms

3차

4K 평균 걸린 시간: 99.40 ms

8K 평균 걸린 시간: 59.60 ms

32K 평균 걸린 시간: 28.20 ms

100MB 평균 걸린 시간: 52.80 ms

4차

4K 평균 걸린 시간: 96.20 ms

8K 평균 걸린 시간: 55.60 ms

32K 평균 걸린 시간: 27.80 ms

100MB 평균 걸린 시간: 50.60 ms

5차

4K 평균 걸린 시간: 99.40 ms

8K 평균 걸린 시간: 62.00 ms

32K 평균 걸린 시간: 29.60 ms

100MB 평균 걸린 시간: 51.60 ms

6차

4K 평균 걸린 시간: 110.80 ms

8K 평균 걸린 시간: 60.00 ms

32K 평균 걸린 시간: 29.60 ms

100MB 평균 걸린 시간: 50.60 ms

7차

4K 평균 걸린 시간: 95.40 ms

8K 평균 걸린 시간: 57.00 ms

32K 평균 걸린 시간: 28.20 ms

100MB 평균 걸린 시간: 44.60 ms

8차

4K 평균 걸린 시간: 102.20 ms

8K 평균 걸린 시간: 56.00 ms

32K 평균 걸린 시간: 30.00 ms

100MB 평균 걸린 시간: 46.60 ms

코드

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;

/**
 * 파일을 A에서 B로 복사할때 버퍼 사이즈가 성능에 영향을 미치는지 테스트한다.
 * 버퍼 사이즈: 4K, 8K, 32K, 그리고 파일크기와 동일한 크기
 */
public class BufferSizeEffectComparison {
    private static final int[] BUFFER_SIZES = {4096, 8192, 32768}; // 4K, 8K, 32K
    private static final String FILE_PATH = Paths.get("src", "main", "java", "yoochul", "week04", "sample.txt").toString();
    private static final String COPY_PATH = Paths.get("src", "main", "java", "yoochul", "week04", "sample_copy.txt").toString();
    private static final int BUFFER_SIZE = 8192;
    private static final int TEST_RUNS = 5;

    public static void main(String[] args) throws IOException {
        generateTestFile(FILE_PATH, 100_000_000); // 100MB file

        warmUpCpu();

        // 4K
        long[] buffer4K = new long[TEST_RUNS];
        for (int i = 0; i < TEST_RUNS; i++) {
            long startTime = System.currentTimeMillis();
            fileNIO(FILE_PATH, COPY_PATH, BUFFER_SIZES[0]);
            long endTime = System.currentTimeMillis();
            buffer4K[i] = endTime - startTime;
        }
        System.out.println("4k 버퍼 시간: " + Arrays.toString(buffer4K));

        // 8K
        long[] buffer8K = new long[TEST_RUNS];
        for (int i = 0; i < TEST_RUNS; i++) {
            long startTime = System.currentTimeMillis();
            fileNIO(FILE_PATH, COPY_PATH, BUFFER_SIZES[1]);
            long endTime = System.currentTimeMillis();
            buffer8K[i] = endTime - startTime;
        }
        System.out.println("8k 버퍼 시간: " + Arrays.toString(buffer8K));

        // 32K
        long[] buffer32K = new long[TEST_RUNS];
        for (int i = 0; i < TEST_RUNS; i++) {
            long startTime = System.currentTimeMillis();
            fileNIO(FILE_PATH, COPY_PATH, BUFFER_SIZES[2]);
            long endTime = System.currentTimeMillis();
            buffer32K[i] = endTime - startTime;
        }
        System.out.println("32k 버퍼 시간: " + Arrays.toString(buffer32K));

        // 파일 통째로 버퍼에 넣기
        long[] bufferSameAsFileSize = new long[TEST_RUNS];
        for (int i = 0; i < TEST_RUNS; i++) {
            long startTime = System.currentTimeMillis();
            fileNIO(FILE_PATH, COPY_PATH, (int) Files.size(Paths.get(FILE_PATH)));
            long endTime = System.currentTimeMillis();
            bufferSameAsFileSize[i] = endTime - startTime;
        }
        System.out.println("파일 사이즈 100MB 버퍼 시간: " + Arrays.toString(bufferSameAsFileSize) + "\n");

        // 평균 시간 측정
        double avg4K = Arrays.stream(buffer4K).average().orElse(0);
        double avg8K = Arrays.stream(buffer8K).average().orElse(0);
        double avg32K = Arrays.stream(buffer32K).average().orElse(0);
        double avg100MB = Arrays.stream(bufferSameAsFileSize).average().orElse(0);
        System.out.printf("4K 평균 걸린 시간: %.2f ms\n", avg4K);
        System.out.printf("8K 평균 걸린 시간: %.2f ms\n", avg8K);
        System.out.printf("32K 평균 걸린 시간: %.2f ms\n", avg32K);
        System.out.printf("100MB 평균 걸린 시간: %.2f ms\n", avg100MB);
    }

    private static void warmUpCpu() {
        int sum = 0;
        for (int i = 0; i < 1_000_000; i++) {
            sum += i;
        }
    }

    /**
     * IO 테스트할 파일을 생성한다. 파일이 존재하면 생성하지 않고 무시한다.
     *
     * @param filePath 파일 생성할 위치
     * @param size 생성할 파일 사이즈
     */
    private static void generateTestFile(String filePath, int size) throws IOException {
        File file = new File(filePath);
        if (file.exists()) {
            return;
        }

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
            char[] buffer = new char[BUFFER_SIZE];
            Arrays.fill(buffer, 'A');
            int written = 0;
            while (written < size) {
                writer.write(buffer, 0, Math.min(BUFFER_SIZE, size - written));
                written += BUFFER_SIZE;
            }
        }
        System.out.println("Test file created.");
    }

    private static void fileNIO(String srcPath, String destPath, int bufferSize) throws IOException {
        try (FileChannel srcChannel = FileChannel.open(Paths.get(srcPath), StandardOpenOption.READ);
             FileChannel destChannel = FileChannel.open(Paths.get(destPath), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {

            ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize);
            while (srcChannel.read(buffer) != -1) {
                buffer.flip();
                while (buffer.hasRemaining()) {
                    destChannel.write(buffer);
                }
                buffer.clear();
            }
        }
    }
}

엇... 파일 크기랑 동일한 버퍼 사이즈가 제일 좋을때도 있네

파일 크기랑 동일한 버퍼 사이즈가 성능이 제일 좋을줄 알았는데 위 예제의 경우 그렇지 않았다.

하지만 아래의 경우 MappedByteBufferRandomAccessFile를 활용하면 파일 크기랑 동일한 버퍼 사이즈가 성능이 제일 좋게나온다. 제일 성능이 좋았던 버퍼 사이즈 32K 보다도 성능이 잘나온다.

실행 결과

횟수
결과

1차

32K 평균 걸린 시간: 46.80 ms

100MB 평균 걸린 시간: 48.40 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 39.40 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 42.80 ms

2차

32K 평균 걸린 시간: 34.00 ms

100MB 평균 걸린 시간: 38.40 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 25.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 26.80 ms

3차

32K 평균 걸린 시간: 45.40 ms

100MB 평균 걸린 시간: 47.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 34.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 38.20 ms

4차

32K 평균 걸린 시간: 38.40 ms

100MB 평균 걸린 시간: 38.60 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 25.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 25.80 ms

5차

32K 평균 걸린 시간: 40.80 ms

100MB 평균 걸린 시간: 54.00 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 39.00 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 37.00 ms

코드

Window에서 실행하니 또 결과가 다르다...

위에 테스트는 Mac에서 진행된 것이다. Window를 이용해 테스트 하니 다음과 같은 결과가 나왔다. 다시 32K 버퍼가 제일 성능이 좋다.

1차

32K 평균 걸린 시간: 38.20 ms

100MB 평균 걸린 시간: 85.20 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 69.60 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 67.80 ms

2차

32K 평균 걸린 시간: 40.00 ms

100MB 평균 걸린 시간: 87.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 74.60 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 68.20 ms

3차

32K 평균 걸린 시간: 38.00 ms

100MB 평균 걸린 시간: 87.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 74.20 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 68.80 ms

4차

32K 평균 걸린 시간: 38.60 ms

100MB 평균 걸린 시간: 88.00 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 74.80 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 69.80 ms

5차

32K 평균 걸린 시간: 39.40 ms

100MB 평균 걸린 시간: 129.20 ms

100MB 평균 걸린 시간 (MappedByteBuffer): 77.00 ms

100MB 평균 걸린 시간 (MappedByteBuffer, RandomAccessFile): 73.20 ms

결론

버퍼 유형, 운영체제 등 여러 요인에 따라서 성능이 달라지니 테스트를 여러번 진행하면 최적화를 찾아가는 과정이 필요하다.

Last updated