본문 바로가기
JAVA/기타

Java Fast I/O (feat. BOJ, BufferedReader, BufferedWriter)

by D.O.T 2024. 7. 6.
BOJ JAVA 풀이에서 왜 BufferedReader, BufferedWriter 를 사용할까?

 

사람들은 PS 중 시간 효율을 조금이라도 올리기 위해서 Fast I/O를 사용한다.

보통 I/O 속도는 Default I/O, Fast I/O, Custom Fast I/O 가 있다.

찐 고수들은 종종 Custom Fast I/O를 사용하는 것을 볼 수 있는데 나는 기본적인 Fast I/O만 사용한다. (초보)

 

Python 에서는 sys.stdin.readline, C++ 에서는 ios_base::sync_with_stdio(0), C에서는 fread() 등

Java에서는 BufferedReader 나 BufferedWriter를 사용하는 것을 볼 수 있다.

 

그럼 왜 사용하는지 한 번 알아보자.

 

BufferedReader vs Scanner

 

BufferedReader

   public class BufferedReader extends Reader {
    private Reader in;

    private char[] cb;

    private static final int DEFAULT_CHAR_BUFFER_SIZE = 8192;
    
    public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }

    public BufferedReader(Reader in) {
        this(in, DEFAULT_CHAR_BUFFER_SIZE);
    }

}

 

자바에서 구현하는 BufferedReader Class 정보다.

기본적으로 8Byte의 char 자료형을 저장할 수 있는 Buffer를 가지고 있고 생성값에 따라 더 늘릴 수도 있다.

한 번에 8Byte의 문자를 read 할 수 있다고 보면 된다.

 

Scanner

public final class Scanner implements Iterator<String>, Closeable {

    private CharBuffer buf;
    private static final int BUFFER_SIZE = 1024;
    private int position;
    private Matcher matcher;
    private Pattern delimPattern;
    private Pattern hasNextPattern;
    private int hasNextPosition;
    private String hasNextResult;
    private Readable source;
    private boolean sourceClosed = false;
    private boolean needInput = false;
    private boolean skipped = false;
    private int savedScannerPosition = -1;
    private Object typeCache = null;
    private boolean matchValid = false;
    private boolean closed = false;
....
}

 

Scanner 이 녀석은 뭐가 너무 많다. 기본적으로 1KB의 Buffer를 가지고 있다.

그 와중에 buffer도 User Defined Data Type이다. 헐

public Scanner(InputStream source) {
    this(new InputStreamReader(source), WHITESPACE_PATTERN);
}

public String nextLine() {
    modCount++;
    if (hasNextPattern == linePattern())
        return getCachedResult();
    clearCaches();

    String result = findWithinHorizon(linePattern, 0);
    if (result == null)
        throw new NoSuchElementException("No line found");
    MatchResult mr = this.match();
    String lineSep = mr.group(1);
    if (lineSep != null)
        result = result.substring(0, result.length() - lineSep.length());
    if (result == null)
        throw new NoSuchElementException();
    else
        return result;
}

 

이건 Scanner 생성자랑 Input Method 코드인데 생성할 때부터 무슨 패턴 검사를하고 입력받을 때도 패턴 검사를 한다.

얘는 그냥 BufferedReader보다 느릴 수 밖에 없다.

 

그럼에도 패턴검사나 기능 측면에서는 장점이 있으니까 사용한다.

 

Performance Test

public class InputTest {

    public static void main(String[] args) throws IOException {
        long startTime, endTime;
        String input = "";
        for (int i = 0; i < 100000; i++) {
            input += "AAAAAAAAAAAAAAAAAAAA";
        }
        System.setIn(new ByteArrayInputStream(input.getBytes()));

        // fread
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        startTime = System.nanoTime();
        String line = br.readLine();
        endTime = System.nanoTime();
        System.out.println("BufferedReader read time: " + (endTime - startTime) + " ns");


        System.setIn(new ByteArrayInputStream(input.getBytes()));

        // read
        Scanner sc = new Scanner(System.in);
        startTime = System.nanoTime();
        line = sc.nextLine();
        endTime = System.nanoTime();
        System.out.println("Scanner read time: " + (endTime - startTime) + " ns");
    }

}

 

BufferedReader read time: 19981900 ns
Scanner read time: 102158800 ns

 

그냥 데이터가 커질수록 무진장 차이난다. 무조건 PS에서는 BufferedReader를 사용해야겠다!

 

BufferedWriter vs System.out

 

BufferedWriter

public class BufferedWriter extends Writer {
    private static final int DEFAULT_INITIAL_BUFFER_SIZE = 512;
    private static final int DEFAULT_MAX_BUFFER_SIZE = 8192;

    private Writer out;

    private char[] cb;
    
    public BufferedWriter(Writer out, int sz) {
        this(out, sz, sz);
    }

    private void implFlushBuffer() throws IOException {
        ensureOpen();
        if (nextChar == 0)
            return;
        out.write(cb, 0, nextChar);
        nextChar = 0;
    }
    
}

 

얘도 마찬가지로 Buffer를 사용하는데 자세하게 뜯어보지 않아도 변수명에 명시해준 덕에 512Byte ~ 8KB의 버퍼가 허용되구나를 예상할 수 있다. 그리고 BufferedWriter 객체를 잘 뜯어보면 write() 메서드를 실행할 때, implFushBuffer() 메서드를 실행하게 된다는 것을 알 수 있다. 그런데 BufferedWriter의 implFushBuffer는 out.write 메서드를 수행하고 있고 out 변수의 객체가 Writer인 것을 알고 있자!

 

System.out.print...

public final class System {
    public static final PrintStream out = null;
}


public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
    private BufferedWriter textOut;
    private OutputStreamWriter charOut;
    
    private void implWrite(String s) throws IOException {
    ensureOpen();
    textOut.write(s);
    textOut.flushBuffer();
    charOut.flushBuffer();
    if (autoFlush && (s.indexOf('\n') >= 0))
        out.flush();
	}
}

 

System 객체에서 출력을 하는 out은 PrintStream 객체이다.

즉, 우리는 PrintStream 객체에서 제공하는 write() 메소드로 출력을하는데 이것을 뜯어보면 BufferedWriter에서 출력을 하고 있다는 것이다! 그럼 BufferedWriter에서 Writer 객체로 출력한다는 것을 알았는데, 결국 System.out 이나 BufferedWriter는 모두 다 Writer 객체로 출력하고 있다는 것이다.

 

그럼 도대체 왜 BufferedWriter를 따로 쓰냐?

1. BufferedWriter는 캐시 크기를 직접 수정할 수 있다.

2. System.out.print()는 매번 Default 크기의 BufferedWirter를 불러와야 한다.

3. System.out.print()는 BufferedWriter의 버퍼 외에 charOut.flushBuffer(), out.flush() 로 스트림을 비우는 추가 작업을 한다.

 

이해하기 쉽도록 설명하자면,

1. BufferedWriter는 1kg ~ 1000kg 용량을 담을 수 있는 장바구니다.

2. System.out.print는 1kg 장바구니만 지원한다. 

이것으로 인해 어떤 차이가 발생할까?

우리가 버퍼를 사용하기 위해서는 OS로부터 메모리를 할당받아야 한다.
1. BufferedWriter로 한 번에 1000kg을 담을 수 있는 장바구니(메모리)를 구한다.
한 번에 1000kg의 물건(문자열)을 담고 바로 전달한다.
대신, 물건(자원))이 1000kg(할당된 메모리량)을 초과할 경우 여러번 전송해야한다.
이 때, 1000kg의 내용물을 비우고 다시 채우는데 상당한 시간이 소요된다.

2. System.out.writer로 한 번에 1kg을 담을 수 있는 장바구니(메모리)를 구한다.
매번 1kg의 물건을 담고 전달한다. 마찬가지로, 한번 전달 시 물건이 1kg을 초과할 경우 내부에서 장바구니를 비우고 다시 채우는 시간이 소요된다.

즉, 대량의 물건(데이터)을 BufferedWriter로 전송할 경우 효과적이다.
소량의 물건도 BufferedWriter로 전송할 경우 효과적이다. (대신 버퍼 크기를 줄여야함)
-> System.out.print.. 의 경우, 스트림도 비우는데 자동차에서 장바구니가 위치할 자리를 깨끗하게 청소한다는 정도로만 생각해도 될 것 같다.

 

Performance Test

package com.study.datastructrue.io;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class OutputTest {

    public static void main(String[] args) throws IOException {
        long bufferStartTime, bufferEndTime;
        long printStartTime, printEndTime;

        int numIterations = 1000;
        String data = "Hello World\n";

        // large buffer, max size = 8 KB
        BufferedWriter bwLarge = new BufferedWriter(new OutputStreamWriter(System.out), 8192);
        long largeBufferStartTime = System.nanoTime();
        for (int i = 0; i < numIterations; i++) {
            bwLarge.write(data);
        }
        bwLarge.flush();
        long largeBufferEndTime = System.nanoTime();

        // default buffer = 512 Byte
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        bufferStartTime = System.nanoTime();
        for (int i = 0; i < numIterations; i++) {
            bw.write(data);
        }
        bw.flush();
        bufferEndTime = System.nanoTime();


        // System.out.print()
        printStartTime = System.nanoTime();
        for (int i = 0; i < numIterations; i++) {
            System.out.print(data);
        }
        printEndTime = System.nanoTime();

        System.out.println("BufferedWriter (default buffer) write time: " + (bufferEndTime - bufferStartTime) + " ns");
        System.out.println("BufferedWriter (large buffer) write time: " + (largeBufferEndTime - largeBufferStartTime) + " ns");
        System.out.println("System.out.print write time: " + (printEndTime - printStartTime) + " ns");
    }
}

 

numIterators 1 10 100 1000 10000
Default Buffer 31100 ns 158900 ns 262700 ns 24384500 ns 196324900 ns
Large Buffer 156400 ns 246900 ns 407100 ns 2100000 ns 281576200 ns
System.out 38800 ns 449300 ns 2504500 ns 26867900 ns 263278000 ns

 

각 PC의 성능마다 결과는 다르겠지만, Buffer가 클수록 비우는데 시간이 꽤 소요가 된다는 것을 알 수가 있다.

PS에서는 대부분 출력결과가 1000개 이하이기 때문에 LargeBuffer로 해도 될 듯,,?