포스트

Buffer와 std::endl 그리고 \n

상황


N과 M 시리즈를 복기하던 중,

5번에서 시간초과가 발생했다

기존 풀이와 차이점은 std::endl\n 밖에 없는데

그렇게 차이가 많이날까? 궁금해서 조사해봤다

스트림(Stream)과 버퍼(Buffer)


img_c_stream

둘이 비교하기에 앞서, 먼저 버퍼(Buffer)에 대한 이해가 필요하다

이전 cin과 getline 글에서 스트림과 버퍼에 대해 간략하게 설명했지만

더 자세히 설명해보면

C++은 파일이나 콘솔의 입출력을 직접 다루지 않고, 스트림이라는 객체를 통해 흐름을 다룬다

스트림은 운영체제에 의해 생성되는 가상의 연결고리를 의미하며,

프로그램과 운영체제의 중간 매개자 역할을 한다

모든 입출력을 바이트의 흐름으로 간주하여 처리한다

즉, 어떤 장치와 프로그램 사이를 잇는 파이프와 같은 존재임

이 스트림 내부에는 버퍼라는 임시 메모리 공간을 가지고 있음

버퍼 사용 장점


  • 문자를 키 하나를 누를때마다 바로 전달하는 것이 아니라, 한번에 묶어서 전달하므로,
    전송시간이 적게 걸려 성능이 향상됨

  • 사용자가 문자를 잘못 입력한 경우 수정할 수 있음

입력 버퍼와 출력 버퍼


img1 daumcdn

  • 임시적으로 버퍼에 저장하는 것을 버퍼링(Buffering)이라 한다

  • 버퍼는 FIFO(First-In First-Out) 방식으로 동작한다

  • 사용자가 키를 누르면 해당 키의 입력 데이터가 입력 버퍼의 맨 뒤에 추가됨

  • 프로그램에서 입력 함수를 호출할 때 마다, 입력 버퍼의 가장 앞에 있는 데이터가 제거됨

  • 입출력 함수를 호출하면, 버퍼에서 데이터가 제거되므로, 같은 데이터를 두번 이상 읽을 수 없음

  • 입출력 버퍼의 크기는 기본적으로 운영체제에 의해 설정되지만,

    std::streambuf::setbuf() 함수과 같이 프로그램에서 직접 버퍼 크기를 설정할 수도 있음

  • 버퍼는 입력 버퍼와 출력 버퍼로 나뉜다

입력 버퍼

  • 키보드와 같은 입력 장치로 부터 들어오는 데이터를 저장하는 공간

  • 표준 입력 스트림인 std::cin은 키보드 입력에 대한 버퍼링을 수행

1
2
3
cin.get(): 버퍼에서 하나의 문자를 읽어들입니다
cin.getline(): 버퍼에서 개행 문자까지 포함한 문자열을 읽어들입니다
cin.ignore(): 버퍼에서 특정 개수의 문자를 무시합니다

출력 버퍼

  • 프로그램에서 출력하는 데이터를 일시적으로 저장하는 공간

  • std::cout은 출력 버퍼를 가지고 있으며,

  • std::endlstd::flush를 사용하거나, 버퍼가 가득 찼을 때 자동으로 버퍼를 비워서 출력

1
2
3
cout.put(): 버퍼에 하나의 문자를 저장합니다
cout.flush(): 버퍼에 저장된 데이터를 출력 장치로 전송합니다
cout.setf(): 버퍼링 방식 등 출력 형식을 설정합니다

std::ios_base::sync_with_stdio

  • 알고리즘 문제 풀 때 std::ios_base::sync_with_stdio(false)를 해주는 이유도

    기본적으로 C++ 표준 라이브러리는 iostream 라이브러리와

    C의 stdio 입출력 기능을 함께 사용할 수 있도록 동기화 되어있다

  • std::ios_base::sync_with_stdio(false)를 작성해주면

    C++의 입출력 스트림과 C표준 입출력 함수의 버퍼가 분리된다

  • C의 버퍼와 병행하여 사용할 수 없지만, 사용하는 버퍼의 수가 줄어 실행 속도가 빨라지게 되는 것!

  • 그래서 이 문장을 사용하면, stdio를 사용하면 안되는 것임

그래서 std::endl과 ‘\n’의 차이는?


1. endl을 사용하여 버퍼를 비움
1
2
3
4
5
int main(void) { 
	std::cout << "hello" << std::endl; 
	sleep(5); 
	return (0); 
}
  • 위 예제는 1. hello를 출력하고, 2. 5초 대기 후 3. 프로그램이 종료된다

  • endl 에 의해 출력 버퍼가 비워진 후, sleep 에 들어가는 것

2. endl을 사용하지 않아 버퍼를 비우지 않음
1
2
3
4
5
int main(void) { 
	std::cout << "hello"; 
	sleep(5); 
	return (0); 
}
  • 두번째 예제는 1. 5초 대기후, 2. hello를 출력후, 3. 프로그램이 종료된다

  • 프로그램이 종료되면서 커널에 의해 버퍼를 강제로 비워지게 된다

  • 즉, sleep 들어가기전에 버퍼에는 “hello”가 들어있지만 출력 함수가 호출되지 않아 버퍼에 남아있는 상태

std::endl은 출력 버퍼를 비우고, \n은 출력 버퍼를 비우지 않는다
버퍼를 비운다는 의미는, buffer에 저장 되었던 내용을 내보내는 것을 의미한다

즉, 버퍼를 비우지 않는다는게, 출력을 안한다는게 아니라,

버퍼에 가지고있느냐, 바로 출력하느냐의 차이였던 것이였다

예제 1번처럼 std::endl`을 통해 바로바로 flush 하느냐

예제 2번처럼 \n처럼 버퍼에 차곡차곡 쌓아뒀다가,

프로그램이 종료시 커널에 의해 한번에 출력하느냐 차이였다

당연히 여러번 출력 함수를 호출하는것 보다는

한번 출력 함수를 호출하는것이 시간적으로 효율적이다

알고리즘 문제같은 경우, 입출력이 큰 상황에서 반복 수행해야할 때,

출력 버퍼를 비우는 작업이 많이 발생하여 시간 초과가 발생할 수 있는 것이였다

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.