C++ 입출력 성능 최적화 (I/O Optimization)
한눈에 보기
cin과 cout이 기본 설정에서 느린 이유를 stdio 동기화와 버퍼 플러시로 설명하고, sync_with_stdio(false), cin.tie(NULL), endl 대신 개행 문자를 쓰는 세 가지 최적화와 주의점을 정리한다.
최종 업데이트 2026.09.17
C++의 표준 입출력 스트림인 cin과 cout은 기본 설정 그대로 쓰면 C의 stdio와 동기화하는 비용 때문에 느리게 동작한다. sync_with_stdio(false)로 동기화를 끊고, cin.tie(NULL)로 cin과 cout의 묶임을 해제하며, 개행에 endl 대신 '\n'을 쓰는 세 가지 조치만으로 입출력 성능을 크게 끌어올릴 수 있다. 알고리즘 문제 풀이처럼 수십만 줄을 입출력하는 상황에서 시간 초과를 가르는 요인이 되기도 한다.
왜 기본 설정의 cin, cout은 느린가
C++ 표준은 printf와 cout을 한 프로그램에서 섞어 써도 출력 순서가 뒤엉키지 않도록, iostream을 stdio와 동기화(Synchronization)해 두었다. 이 동기화 모드에서는 cout이 자체 버퍼(Buffer)를 적극적으로 활용하지 못하고 stdio 쪽과 보조를 맞추느라 문자 단위에 가까운 처리를 하게 되어 오버헤드가 크다. 또한 cin은 기본적으로 cout에 묶여(tie) 있어서, 입력을 받기 직전마다 cout의 버퍼를 강제로 비운다(flush). 프롬프트를 먼저 보여 주고 입력을 받는 대화형 프로그램에는 자연스러운 동작이지만, 입력과 출력이 대량으로 반복되는 프로그램에서는 불필요한 플러시가 누적되어 성능을 갉아먹는다.
세 가지 최적화 조치
#include <iostream>
int main() {
std::ios_base::sync_with_stdio(false); /* stdio와의 동기화 해제 */
std::cin.tie(NULL); /* 입력 전 자동 flush 해제 */
int n;
std::cin >> n;
for (int i = 0; i < n; i++) {
int x;
std::cin >> x;
std::cout << x * 2 << '\n'; /* endl 대신 '\n' */
}
return 0;
}
sync_with_stdio(false)는 iostream이 stdio와 독립적으로 자체 버퍼를 운용하게 만들어 입출력을 묶음 단위로 처리하게 한다. cin.tie(NULL)은 cin이 입력 전에 cout을 플러시하는 연결을 끊는다. 마지막으로 endl은 개행 문자 출력에 더해 버퍼 플러시까지 수행하는 조작자(Manipulator)이므로, 반복문 안에서 쓰면 매 줄마다 시스템 호출(System Call) 수준의 비용이 발생한다. 단순 개행이 목적이라면 '\n'만 출력하고 플러시는 버퍼가 찼을 때나 프로그램 종료 시점에 맡기는 것이 효율적이다.
버퍼링을 메모리 관점에서 보기
입출력이 느린 근본 이유는 커널(Kernel)로 진입하는 시스템 호출 비용에 있다. write를 문자 하나마다 호출하면 사용자 모드와 커널 모드 전환이 그만큼 반복된다. 그래서 표준 라이브러리는 사용자 공간에 버퍼를 두고 출력을 모았다가 버퍼가 가득 차면 한 번의 시스템 호출로 내보낸다. 예를 들어 4KB 버퍼라면 시스템 호출 횟수가 수천분의 일로 줄어드는 셈이다. endl로 매번 플러시하는 것은 이 버퍼링 구조를 스스로 무력화하는 행위다. 입력도 마찬가지로 커널에서 한 번에 큰 덩어리를 읽어 와 버퍼에서 파싱(Parsing)하는 구조이므로, 동기화 해제는 이 버퍼를 온전히 활용하게 해 준다.
주의할 점
동기화를 끊은 뒤에는 printf, scanf와 cin, cout을 섞어 쓰면 안 된다. 서로 다른 버퍼를 쓰게 되어 출력 순서가 보장되지 않기 때문이다. 또한 이 설정들은 스레드(Thread) 동기화와는 무관하며, 멀티스레드 환경에서 스트림을 공유할 때의 안전성 문제는 별도로 다뤄야 한다. 대화형 프로그램에서 tie를 해제하면 프롬프트가 화면에 나오기 전에 입력 대기가 걸릴 수 있으므로, 필요한 시점에 std::flush로 직접 플러시해야 한다. 디버깅 중에는 플러시가 늦어져 크래시 직전 출력이 화면에 안 보일 수 있다는 점도 알아 두면 좋다.
| 조치 | 효과 | 대가 |
|---|---|---|
| sync_with_stdio(false) | iostream 자체 버퍼 활용으로 속도 향상 | stdio 계열과 혼용 불가 |
| cin.tie(NULL) | 입력 전 불필요한 flush 제거 | 프롬프트 출력 시점을 직접 관리 |
| endl 대신 '\n' | 줄마다 발생하던 flush 제거 | 즉시 출력이 필요하면 직접 flush |
이 세 가지로도 부족한 극단적인 상황에서는 scanf와 printf로 회귀하거나, getchar 계열 함수 또는 read 시스템 호출로 큰 버퍼를 직접 읽어 파싱하는 수동 입출력(Fast IO)을 구현하기도 한다. 다만 일반 애플리케이션에서는 가독성과 타입 안전성을 해치면서까지 갈 필요가 없고, 표준 조치 세 가지면 충분한 경우가 대부분이다.
면접에서 이렇게 물어본다
- sync_with_stdio(false)는 무엇을 하며, 왜 성능이 좋아지는가?
- cin.tie(NULL)이 끊는 연결은 무엇이고, 끊었을 때 주의할 점은 무엇인가?
- endl과 '\n'의 차이를 버퍼 플러시 관점에서 설명해 보라.
- 버퍼링이 입출력 성능을 높이는 원리를 시스템 호출 비용과 연결해 설명해 보라.
- 동기화를 해제한 뒤 printf와 cout을 섞어 쓰면 어떤 문제가 생기는가?
C / C++에서 같이 읽기
- C 언어 컴파일 과정 (Compile Process)C 소스 코드가 전처리, 컴파일, 어셈블, 링킹 네 단계를 거쳐 실행 파일이 되는 과정을 단계별 산출물, 목적 파일의 섹션 구조, 정적 링킹과 동적 링킹의 차이와 함께 정리한다.
- C 동적 할당 (malloc, free)실행 중에 힙 메모리를 확보하고 반납하는 C의 동적 할당을 malloc, calloc, realloc, free의 동작과 할당기의 내부 구조, 메모리 누수와 댕글링 포인터 같은 대표 오류 중심으로 설명한다.
- 포인터 (Pointer)다른 변수의 메모리 주소를 저장하는 포인터의 개념과 역참조, 포인터 산술, 배열과의 관계, 이중 포인터와 함수 포인터, 그리고 널 포인터와 댕글링 포인터 같은 오류 상황까지 정리한다.
- 구조체 메모리 크기와 정렬, 패딩 (Struct Alignment)구조체 크기가 멤버 합보다 커지는 이유인 메모리 정렬과 패딩의 규칙을 오프셋 계산 예시로 설명하고, 멤버 순서에 따른 크기 차이와 #pragma pack 사용 시 주의점을 다룬다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기