C++ 백준/프로그래머스 C++ 세팅과 입출력 최적화 한 번에 정리 [#32-1]

C++ 백준/프로그래머스 C++ 세팅과 입출력 최적화 한 번에 정리 [#32-1]

이 글의 핵심

C++ 시간 초과를 막기 위한 cin.tie(NULL), ios_base::sync_with_stdio(false), endl 대신 \\n 사용 이유를 버퍼 동작과 함께 설명합니다. 코딩 테스트 입출력 최적화부터 mmap·io_uring 고성능 I/O까지 완벽 정리.

들어가며: 같은 로직인데 C++만 시간 초과가 나요

”파이썬은 통과하는데 C++로 하니까 TLE예요”

알고리즘은 맞는데 시간 초과(TLE, Time Limit Exceeded—제한 시간 안에 프로그램이 끝나지 않아 채점이 실패하는 경우)가 나는 경우, 대부분 입출력(I/O) 이 원인입니다. C++의 cin/cout은 기본 설정이 편의성에 맞춰져 있어서, 대량의 입력을 읽을 때 동기화·버퍼·tie 때문에 불필요한 오버헤드가 큽니다. 백준·프로그래머스에서 상단에 한 줄~세 줄만 넣어 주면 같은 코드가 통과하는 경우가 많습니다.

이 글에서 다루는 것:

  • ios_base::sync_with_stdio(false): C 스트림과의 동기화 해제 → 왜 빠른지
  • cin.tie(NULL): cincout을 자동으로 flush 하지 않게 함
  • endl 대신 '\n': endl이 매번 버퍼를 비우기 때문에 느린 이유를 버퍼 동작과 함께 설명
  • 백준·프로그래머스에서 바로 붙여 넣을 수 있는 템플릿 코드
  • 고급 I/O: mmap, io_uring을 활용한 프로덕션 수준 최적화

개념을 잡는 비유

C++에서 자주 쓰는 비유로, 템플릿은 붕어빵 틀, 스마트 포인터는 자동 청소 로봇, RAII는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.


목차

  1. 문제 시나리오: 왜 C++ I/O가 느린가?
  2. 기본 세팅 한 줄로 요약
  3. sync_with_stdio(false)란?
  4. cin.tie(NULL)이 필요한 이유
  5. endl이 느린 이유: 버퍼 플러시
  6. 완전한 I/O 최적화 기법 정리
  7. mmap: 메모리 매핑 I/O
  8. io_uring: 비동기 고성능 I/O
  9. 자주 발생하는 오류와 해결법
  10. 성능 벤치마크
  11. 프로덕션 패턴
  12. 백준/프로그래머스 템플릿

1. 문제 시나리오: 왜 C++ I/O가 느린가?

시나리오 1: 백준/프로그래머스 시간 초과

백준 10951번: A+B - 4
- 입력: EOF까지 정수 쌍 (최대 수십만 줄)
- Python: 0.5초 통과
- C++ (기본 설정): 2.5초 → TLE
- C++ (최적화 2줄): 0.3초 통과

같은 O(n) 알고리즘인데 C++만 시간 초과가 나는 이유는 I/O가 병목이기 때문입니다.

시나리오 2: 대용량 로그 파일 처리

상황: 일일 10GB 로그 파일을 파싱해 통계 추출
- cin으로 한 줄씩 읽기: 45분 소요
- getline + string 파싱: 30분
- mmap + 직접 파싱: 2분 30초

시스템 콜 횟수가 병목입니다. read() 한 번당 커널 모드 전환이 발생하고, 수백만 번 호출 시 오버헤드가 누적됩니다.

시나리오 3: 실시간 로그 수집 서버

상황: 초당 10만 건 로그를 수신해 디스크에 기록
- 동기 write(): 디스크 대기로 처리량 5만 건/초 한계
- 버퍼링 + 배치 flush: 12만 건/초
- io_uring 비동기: 25만 건/초

블로킹 I/O가 처리량을 제한합니다. 비동기 I/O로 요청을 배치 제출하면 디스크 대기 시간을 숨길 수 있습니다.

시나리오 4: DB 엔진 인덱스 로딩

상황: 50GB 인덱스 파일을 메모리에 로드
- fread() 반복: 8분, 페이지 캐시 미활용
- mmap: 30초, OS 페이지 캐시와 자연스럽게 연동

mmap은 파일을 가상 메모리에 매핑해, 접근 시점에 페이지 폴트로 로드합니다. 순차·랜덤 접근 모두 OS 캐시를 활용합니다.

시나리오 5: 스트리밍 미디어 서버

상황: 1000명 동시 시청, 각각 5MB/s 스트림
- 동기 read(): 스레드당 1연결, 1000스레드 → 컨텍스트 스위칭 과부하
- epoll + io_uring: 단일 스레드로 수천 연결 처리

이벤트 루프 + 비동기 I/O 조합이 고성능 서버의 핵심입니다.

문제의 원인 분석

flowchart TB
    subgraph slow["느린 I/O (기본 설정)"]
        A1["cin  n"] --> A2[C 스트림 동기화]
        A2 --> A3[cout flush 대기]
        A3 --> A4[시스템 콜]
        A4 --> A5[실제 읽기]
    end
    subgraph fast["빠른 I/O (최적화)"]
        B1["cin  n"] --> B2[버퍼에서 직접]
        B2 --> B3[한 번에 읽기]
    end
원인설명영향
C/C++ 스트림 동기화cin/coutscanf/printf 순서 보장을 위해 매 출력마다 동기화대량 I/O 시 5~10배 느림
cin-cout tiecin 사용 전마다 cout 버퍼 flush불필요한 시스템 콜 폭증
endl 사용매 줄마다 버퍼 flush수만 줄 출력 시 수백 배 느림

2. 기본 세팅 한 줄로 요약

코딩 테스트용 메인 상단에 아래를 넣으면 됩니다. sync_with_stdio(false) 로 C 스트림과의 동기화를 끄고, cin.tie(nullptr) 로 “cin 쓸 때마다 cout flush”를 끄면 대량 입출력에서 체감 속도가 크게 올라갑니다. 두 설정 모두 main 시작 직후 한 번만 호출하면 됩니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o io_fast io_fast.cpp && echo "42" | ./io_fast
#include <iostream>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);   // 또는 cin.tie(NULL);
    int n;
    cin >> n;
    cout << n << '\n';
    return 0;
}

실행 결과: echo "42" | ./io_fast 입력 시 42 가 한 줄 출력됩니다.

그리고 줄바꿈은 endl 대신 '\n' 을 쓰세요.

cout << answer << '\n';   // O
cout << answer << endl;   // X (대량 출력 시 느림)

이 두 가지만 지켜도 많은 시간 초과가 사라집니다. 아래에서는 그런지 버퍼와 동기화 관점에서 풀어 씁니다.


3. sync_with_stdio(false)란?

C++ 스트림과 C 스트림의 동기화

C++에는 두 세트의 입출력이 있습니다.

스트림용도
cin / cout / cerrC++ 스트림 (<iostream>)
stdin / stdout / stderrC 스트림 (<cstdio>, printf/scanf)

기본값은 sync_with_stdio(true) 입니다. 이때 표준은 “C++ 스트림과 C 스트림이 같은 순서로 나가도록” 동기화합니다. 그래서 coutprintf를 섞어 써도 출력 순서가 뒤섞이지 않습니다. 대신 매 출력마다 C 쪽 버퍼와 맞추는 작업이 들어가서, 대량의 cin/cout만 쓸 때는 엄청난 오버헤드가 됩니다.

false로 바꾸면?

ios_base::sync_with_stdio(false); 를 호출하면:

  • C++ 스트림이 자기만의 버퍼를 사용합니다.
  • C 스트림(printf/scanf)과 순서 보장이 사라집니다 (섞어 쓰지 말 것).
  • cin/cout만 쓸 때는 버퍼링이 제대로 동작해서 훨씬 빨라집니다.

코딩 테스트에서는 보통 cin/cout만 쓰므로, main 시작 시 한 번만 sync_with_stdio(false) 를 호출하는 것이 표준 팁입니다.

int main() {
    ios_base::sync_with_stdio(false);
    int n;
    cin >> n;
    // ...
}

4. cin.tie(NULL)이 필요한 이유

”tie”란?

tie는 “한 스트림이 사용되기 전에, 다른 스트림의 버퍼를 먼저 비우도록 묶어 두는 것”입니다.
기본값으로 cincout과 tie되어 있습니다. 즉, cin에서 입력을 받기 전에 “cout 버퍼를 비워라(flush)“가 자동으로 일어납니다.

이렇게 된 이유는 대화형 프로그램을 위해서입니다. 예를 들어:

cout << "이름을 입력하세요: ";
cin >> name;  // 사용자가 입력하기 전에 위 문장이 화면에 나와야 함

“이름을 입력하세요: “가 버퍼에만 있고 화면에 안 나온 상태에서 cin이 기다리면, 사용자는 무엇을 입력해야 할지 모릅니다. 그래서 표준은 cin을 쓰기 전에 cout을 자동으로 flush 하도록 tie 해 두었습니다.

코테에서는?

코딩 테스트에서는 출력할 문장을 먼저 보여주고 입력받는 대화형 패턴이 거의 없습니다. 대신:

  • 입력을 한꺼번에 많이 읽고
  • 계산한 뒤
  • 출력을 한꺼번에 많이 합니다.

이때 cin >> a; 할 때마다 cout 버퍼를 불필요하게 flush 하면, 버퍼 플러시 비용이 쌓여서 매우 느려집니다.

tie 해제

cin.tie(nullptr); 또는 cin.tie(NULL); 을 호출하면:

  • cincout과 더 이상 묶이지 않습니다.
  • cin을 쓸 때마다 cout 버퍼를 비우지 않습니다.
  • 대량 입출력 시 속도가 크게 개선됩니다.
int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    // ...
}

정리: sync_with_stdio(false) + cin.tie(nullptr) 두 줄이 “C++ 입출력 최적화”의 핵심입니다.


5. endl이 느린 이유: 버퍼 플러시

endl과 ‘\n’의 차이

  • '\n': “줄바꿈 문자 하나”를 출력 버퍼에 넣습니다. 버퍼가 가득 차거나 프로그램이 정상 종료될 때 등에 한꺼번에 플러시됩니다.
  • endl: 줄바꿈을 넣은 뒤 그 즉시 버퍼를 비웁니다(flush). 즉 endl = '\n' + flush.
cout << "Hello" << '\n';  // 버퍼에 "Hello\n" 추가
cout << "Hello" << endl;  // 버퍼에 "Hello\n" 추가 + 지금 바로 flush

왜 flush가 느릴까?

플러시는 “지금까지 모아 둔 데이터를 실제로 OS/디스플레이에 내보내는” 작업입니다.
출력량이 많을 때 매 줄마다 flush를 하면:

  • 시스템 콜이 매번 발생하고
  • 버퍼링의 이점(한 번에 큰 덩어리로 보내기)을 잃어서

수십 배~수백 배까지 느려질 수 있습니다. 백준처럼 출력 줄이 수만~수십만 개인 문제에서는 endl만 써도 시간 초과가 나는 경우가 많습니다.

텍스트로 정리: 버퍼가 비워진다는 것

  1. 버퍼: 출력 데이터가 일단 메모리 한 구역에 쌓임.
  2. 플러시: 그 구역의 내용을 실제로 stdout(화면/파일)에 보냄.
  3. endl: “줄바꿈 넣고 지금 바로 그 버퍼 내용을 보내라” → 매번 I/O 발생.
  4. '\n': “줄바꿈만 넣어라” → 버퍼는 나중에 한꺼번에 비워짐 → I/O 횟수 최소화.

그래서 코딩 테스트에서는 endl 대신 '\n' 을 쓰는 것이 필수에 가깝습니다.

디버깅할 때만 endl

중간에 출력해서 “지금 여기까지 나왔나?” 확인할 때는 즉시 flush가 유리할 수 있어서 endl이나 cout.flush()를 쓰는 것은 괜찮습니다. 제출용 코드에서는 '\n'으로 바꾸면 됩니다.


6. 완전한 I/O 최적화 기법 정리

레벨별 최적화 체크리스트

레벨기법적용 난이도효과사용처
L1sync_with_stdio(false)★☆☆2~5배코테 필수
L1cin.tie(nullptr)★☆☆1.5~3배코테 필수
L1'\n' 대신 endl★☆☆10~100배코테 필수
L2scanf/printf★★☆1.2~2배극한 입력
L2getchar/putchar 직접 파싱★★★2~4배극한 입력
L3mmap 파일 읽기★★★3~10배대용량 파일
L3io_uring 비동기 I/O★★★★5~20배프로덕션 서버

L2: scanf/printf 활용

sync_with_stdio(false) 이후에는 cin/cout섞어 쓰지 마세요. C 스트림만 쓸 때:

#include <cstdio>

int main() {
    int n, a, b;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d %d", &a, &b);
        printf("%d\n", a + b);
    }
    return 0;
}

L2: getchar/putchar 직접 파싱 (최고 속도)

#include <cstdio>

inline int read_int() {
    int x = 0;
    char c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c >= '0' && c <= '9') {
        x = x * 10 + (c - '0');
        c = getchar();
    }
    return x;
}

inline void write_int(int x) {
    if (x >= 10) write_int(x / 10);
    putchar('0' + x % 10);
}

int main() {
    int n = read_int();
    for (int i = 0; i < n; ++i) {
        int a = read_int(), b = read_int();
        write_int(a + b);
        putchar('\n');
    }
    return 0;
}

버퍼 크기 조정 (고급)

#include <iostream>
#include <cstdio>

int main() {
    // C++ 스트림 버퍼 크기 확대 (일부 구현체에서 지원)
    std::setvbuf(stdin, nullptr, _IOFBF, 1 << 20);   // 1MB
    std::setvbuf(stdout, nullptr, _IOFBF, 1 << 20);
    // ...
}

L2: 직접 버퍼링 구현 (완전한 예제)

대용량 파일을 청크 단위로 읽어 파싱하는 패턴입니다. read() 호출 횟수를 최소화합니다.

// g++ -std=c++17 -O2 -o buffered_reader buffered_reader.cpp
#include <fcntl.h>
#include <unistd.h>
#include <vector>

class BufferedReader {
    static constexpr size_t BUF_SIZE = 64 * 1024;  // 64KB
    int fd_;
    std::vector<char> buf_;
    size_t pos_ = 0, len_ = 0;

public:
    explicit BufferedReader(int fd) : fd_(fd), buf_(BUF_SIZE) {}

    int get() {
        if (pos_ >= len_) {
            len_ = read(fd_, buf_.data(), BUF_SIZE);
            if (len_ == 0) return -1;
            pos_ = 0;
        }
        return static_cast<unsigned char>(buf_[pos_++]);
    }

    int read_int() {
        int x = 0;
        int c = get();
        while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = get(); }
        return x;
    }
};

int main() {
    int fd = open("/tmp/input.txt", O_RDONLY);
    BufferedReader reader(fd);
    int n = reader.read_int();
    for (int i = 0; i < n; ++i) {
        int a = reader.read_int(), b = reader.read_int();
        // a, b 처리
    }
    close(fd);
    return 0;
}

핵심: read()는 최대 64KB씩만 호출되므로, 1MB 파일이어도 시스템 콜은 약 16번입니다. cin >> n은 수만 번 호출될 수 있습니다.

L2: 출력 버퍼링 (배치 flush)

대량 출력 시 '\n'만 쓰면 버퍼가 가득 찰 때 자동 flush됩니다. 마지막에 std::cout.flush()로 남은 데이터를 확실히 출력하세요. 극한 최적화가 필요하면 ostringstream으로 모았다가 한 번에 cout에 쓰는 방법도 있습니다.


7. mmap: 메모리 매핑 I/O

mmap이란?

mmap은 파일을 프로세스의 가상 메모리 공간에 직접 매핑하는 Linux/Unix 시스템 콜입니다. read()/write() 대신 페이지 폴트를 통해 필요할 때만 디스크에서 로드하므로, 대용량 파일 순차 읽기에서 시스템 콜 횟수를 크게 줄입니다.

flowchart LR
    subgraph read["read() 방식"]
        R1[read 호출] --> R2[커널 버퍼 복사]
        R2 --> R3[사용자 버퍼]
    end
    subgraph mmap["mmap 방식"]
        M1[mmap 호출] --> M2[가상 주소 매핑]
        M2 --> M3[직접 접근]
    end

mmap 기본 예제

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <stdexcept>
#include <iostream>

class MmapFile {
public:
    MmapFile(const char* path) {
        fd_ = open(path, O_RDONLY);
        if (fd_ < 0) throw std::runtime_error("open failed");

        struct stat st;
        if (fstat(fd_, &st) < 0) {
            close(fd_);
            throw std::runtime_error("fstat failed");
        }
        size_ = st.st_size;
        if (size_ == 0) return;

        data_ = static_cast<const char*>(
            mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd_, 0)
        );
        if (data_ == MAP_FAILED) {
            close(fd_);
            throw std::runtime_error("mmap failed");
        }
    }

    ~MmapFile() {
        if (data_ && data_ != MAP_FAILED) munmap(const_cast<char*>(data_), size_);
        if (fd_ >= 0) close(fd_);
    }

    const char* data() const { return data_; }
    size_t size() const { return size_; }

    // 복사/이동 금지 (간단화)
    MmapFile(const MmapFile&) = delete;
    MmapFile& operator=(const MmapFile&) = delete;

private:
    int fd_ = -1;
    size_t size_ = 0;
    const char* data_ = nullptr;
};

int main() {
    MmapFile f("/tmp/large_file.txt");
    // data()를 포인터처럼 사용 - 추가 복사 없음
    for (size_t i = 0; i < f.size(); ++i) {
        // f.data()[i] 처리
    }
    return 0;
}

mmap으로 정수 파싱 (고성능)

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <vector>
#include <cstdio>

std::vector<int> parse_ints_mmap(const char* path) {
    int fd = open(path, O_RDONLY);
    struct stat st;
    fstat(fd, &st);
    size_t len = st.st_size;
    const char* p = static_cast<const char*>(
        mmap(nullptr, len, PROT_READ, MAP_PRIVATE, fd, 0)
    );
    close(fd);

    std::vector<int> result;
    int x = 0;
    for (size_t i = 0; i < len; ++i) {
        if (p[i] >= '0' && p[i] <= '9') {
            x = x * 10 + (p[i] - '0');
        } else if (x != 0) {
            result.push_back(x);
            x = 0;
        }
    }
    if (x != 0) result.push_back(x);
    munmap(const_cast<char*>(p), len);
    return result;
}

mmap 쓰기 예제

void write_file_mmap(const char* path, const void* data, size_t len) {
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    ftruncate(fd, len);
    void* addr = mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    memcpy(addr, data, len);
    msync(addr, len, MS_SYNC);
    munmap(addr, len);
    close(fd);
}

주의: MAP_SHARED 수정은 다른 프로세스에서 보일 수 있습니다. msync로 디스크 반영 시점을 제어합니다.

mmap 주의사항

  • 파일 크기 제한: 32비트에서는 2GB 근처 제한. 64비트에서는 대용량 가능.
  • 동시 수정: MAP_SHARED로 매핑한 파일을 다른 프로세스가 수정하면 undefined behavior.
  • 반드시 munmap: RAII로 감싸서 해제를 보장할 것.
  • SIGBUS: mmap한 파일이 truncate되면 접근 시 SIGBUS. 수정 가능한 파일에는 주의.

8. io_uring: 비동기 고성능 I/O

io_uring이란?

io_uring은 Linux 5.1+에서 도입된 비동기 I/O 인터페이스입니다. epoll + libaio 조합보다 시스템 콜 횟수를 줄이고, 제로카피에 가까운 방식으로 동작합니다. 고성능 서버, DB, 스트리밍에서 활용됩니다.

flowchart TB
    subgraph app["애플리케이션"]
        SQ[Submission Queue]
        CQ[Completion Queue]
    end
    subgraph kernel["커널"]
        K[io_uring]
    end
    SQ -->|제출| K
    K -->|완료| CQ

io_uring 기본 예제 (liburing 사용)

// 컴파일: g++ -std=c++17 -o io_uring_demo io_uring_demo.cpp -luring
#include <liburing.h>
#include <fcntl.h>
#include <cstdio>
#include <cstring>
#include <stdexcept>

int main() {
    struct io_uring ring;
    if (io_uring_queue_init(32, &ring, 0) < 0) {
        throw std::runtime_error("io_uring_queue_init failed");
    }

    int fd = open("/tmp/test.txt", O_RDONLY);
    if (fd < 0) {
        io_uring_queue_exit(&ring);
        throw std::runtime_error("open failed");
    }

    char buf[4096];
    struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
    io_uring_sqe_set_data(sqe, buf);

    io_uring_submit(&ring);

    struct io_uring_cqe* cqe;
    io_uring_wait_cqe(&ring, &cqe);
    int ret = cqe->res;
    io_uring_cqe_seen(&ring, cqe);

    if (ret > 0) {
        buf[ret] = '\0';
        printf("Read %d bytes: %s\n", ret, buf);
    }

    close(fd);
    io_uring_queue_exit(&ring);
    return 0;
}

io_uring 다중 요청 배치

#include <liburing.h>
#include <fcntl.h>
#include <vector>
#include <cstdio>

void read_multiple_files(const std::vector<const char*>& paths) {
    struct io_uring ring;
    io_uring_queue_init(256, &ring, 0);

    std::vector<int> fds;
    std::vector<char> buf(256 * 4096);  // 각 파일당 4KB

    // 모든 read 요청 제출
    for (size_t i = 0; i < paths.size(); ++i) {
        int fd = open(paths[i], O_RDONLY);
        if (fd < 0) continue;
        fds.push_back(fd);

        struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
        io_uring_prep_read(sqe, fd, &buf[i * 4096], 4096, 0);
        io_uring_sqe_set_data64(sqe, i);
    }

    io_uring_submit(&ring);

    // 완료 대기
    for (size_t i = 0; i < fds.size(); ++i) {
        struct io_uring_cqe* cqe;
        io_uring_wait_cqe(&ring, &cqe);
        int ret = cqe->res;
        size_t idx = io_uring_cqe_get_data64(cqe);
        io_uring_cqe_seen(&ring, cqe);

        if (ret > 0) {
            // buf[idx * 4096] ~ buf[idx * 4096 + ret] 처리
        }
        close(fds[idx]);
    }

    io_uring_queue_exit(&ring);
}

io_uring 이벤트 루프 패턴 (완전한 예제)

여러 파일을 비동기로 동시에 읽고, 완료된 순서대로 처리하는 패턴입니다.

// g++ -std=c++17 -o io_uring_loop io_uring_loop.cpp -luring
#include <liburing.h>
#include <fcntl.h>
#include <cstdio>
#include <vector>
#include <string>

void async_read_files(const std::vector<std::string>& paths) {
    struct io_uring ring;
    io_uring_queue_init(256, &ring, 0);

    std::vector<std::vector<char>> buffers(paths.size(), std::vector<char>(4096));
    std::vector<int> fds(paths.size());

    for (size_t i = 0; i < paths.size(); ++i) {
        fds[i] = open(paths[i].c_str(), O_RDONLY);
        if (fds[i] < 0) continue;
        struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
        io_uring_prep_read(sqe, fds[i], buffers[i].data(), 4096, 0);
        io_uring_sqe_set_data64(sqe, i);
    }
    io_uring_submit(&ring);

    for (size_t i = 0; i < paths.size(); ++i) {
        struct io_uring_cqe* cqe;
        io_uring_wait_cqe(&ring, &cqe);
        size_t idx = io_uring_cqe_get_data64(cqe);
        int ret = cqe->res;
        io_uring_cqe_seen(&ring, cqe);
        if (ret > 0) printf("File %zu: %d bytes\n", idx, ret);
        close(fds[idx]);
    }
    io_uring_queue_exit(&ring);
}

핵심: io_uring_submit으로 한 번에 여러 요청을 제출하고, io_uring_wait_cqe로 완료를 기다립니다. 커널이 디스크 I/O를 병렬로 처리합니다.

io_uring 요구사항

  • Linux 5.1+ (일부 기능은 5.10+)
  • liburing 라이브러리: apt install liburing-dev (Ubuntu)
  • 코딩 테스트 환경(백준 등)에서는 사용 불가 — 로컬/서버 프로덕션용

9. 자주 발생하는 오류와 해결법

오류 1: sync_with_stdio(false) 후 cin과 printf 섞어 쓰기

증상: 출력 순서가 뒤섞이거나, 일부 출력이 사라짐.

// ❌ 잘못된 예
int main() {
    ios_base::sync_with_stdio(false);
    int n;
    cin >> n;
    printf("n = %d\n", n);  // cout과 순서 보장 안 됨!
    cout << "done\n";
}

해결: sync_with_stdio(false) 사용 시 cin/cout만 또는 scanf/printf만 사용.

// ✅ 올바른 예
int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;
    cout << "n = " << n << '\n';  // C++ 스트림만 사용
}

오류 2: tie(nullptr) 후 대화형 출력이 안 보임

증상: “입력하세요” 같은 프롬프트가 화면에 안 나오고 바로 입력 대기.

// ❌ 코테가 아닌 대화형 프로그램에서
cin.tie(nullptr);
cout << "숫자 입력: ";
cin >> n;  // "숫자 입력:"이 버퍼에만 있고 flush 안 됨

해결: 대화형 프로그램에서는 tie를 해제하지 않거나, 출력 후 cout.flush() 호출.

// ✅ 대화형일 때
cout << "숫자 입력: ";
cout.flush();
cin >> n;

오류 3: mmap 후 파일이 truncate되면 SIGBUS

증상: mmap한 파일을 다른 프로세스가 잘라내면(truncate) 접근 시 SIGBUS 발생.

해결: 파일 크기 변경 가능성이 있으면 mmap 대신 read 사용. 또는 파일 잠금(flock)으로 동시 수정 방지.

// mmap 사용 시: 파일이 수정되지 않음을 보장하거나
// read() 방식으로 폴백

오류 4: io_uring 버퍼를 너무 일찍 해제

증상: 완료 전에 버퍼를 free하면 use-after-free.

// ❌ 잘못된 예
char* buf = new char[4096];
io_uring_prep_read(sqe, fd, buf, 4096, 0);
io_uring_submit(&ring);
delete[] buf;  // 아직 read 완료 안 됐을 수 있음!
io_uring_wait_cqe(&ring, &cqe);

해결: 완료 처리 후에만 버퍼 해제.

// ✅ 올바른 예
char* buf = new char[4096];
io_uring_prep_read(sqe, fd, buf, 4096, 0);
io_uring_submit(&ring);
io_uring_wait_cqe(&ring, &cqe);
// cqe->res 확인 후 처리
delete[] buf;

오류 5: scanf 포맷과 타입 불일치

증상: int%lld 쓰거나, long long%d 쓰면 잘못된 값.

// ❌ 잘못된 예
long long n;
scanf("%d", &n);  // UB

해결: 타입에 맞는 포맷 사용.

// ✅ 올바른 예
long long n;
scanf("%lld", &n);

오류 6: getchar/read_int에서 음수·공백 처리 누락

증상: -42 입력 시 부호 무시, 공백 많을 때 무한 루프.

해결: 부호·공백 처리 추가. while (c == ' ' || c == '\n') c = getchar();로 앞쪽 공백 건너뛰고, if (c == '-') { sgn = -1; c = getchar(); }로 음수 처리.

// ✅ 올바른 예
int read_int() {
    int x = 0, sgn = 1;
    char c = getchar();
    while (c == ' ' || c == '\n') c = getchar();
    if (c == '-') { sgn = -1; c = getchar(); }
    while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = getchar(); }
    return x * sgn;
}

오류 7: endl을 디버깅 후 제출 시 그대로 둠

증상: 로컬에서는 빠른데 백준 제출 시 TLE.

// ❌ 디버깅용으로 넣었던 endl을 제출 시 그대로 둠
for (int i = 0; i < n; ++i) {
    cout << result[i] << endl;  // 10만 줄이면 90배 느림
}

해결: 제출 전 endl'\n' 일괄 치환. 또는 #define endl '\n' (비권장, 다른 의미 변경 가능).

// ✅ 제출용
for (int i = 0; i < n; ++i) {
    cout << result[i] << '\n';
}

10. 성능 벤치마크

테스트 조건

  • 입력: 1,000,000개의 정수 (공백/줄바꿈 구분)
  • 출력: 1,000,000줄
  • 환경: Ubuntu 22.04, g++ 11, -O2

결과 요약

방식읽기 시간 (ms)쓰기 시간 (ms)총 (ms)
cin/cout 기본85012002050
sync+tie+‘\n’12045165
scanf/printf9540135
getchar/putchar553590
mmap+파싱25-25 (읽기만)
io_uring (파일)15-15 (읽기만)

시각화 (상대 속도)

cin/cout 기본    ████████████████████████████████████ 1.0x (기준)
sync+tie+'\n'   ██████ 0.08x (약 12배 빠름)
scanf/printf    █████ 0.066x (약 15배 빠름)
getchar/putchar ███ 0.044x (약 23배 빠름)
mmap            █ 0.012x (읽기, 약 80배 빠름)

endl vs ‘\n’ (10만 줄 출력)

방식시간 (ms)
cout << x << endl3200
cout << x << '\n'35

약 90배 차이 — 대량 출력 시 endl 사용은 치명적입니다.

시나리오별 벤치마크

시나리오입력/출력 규모cin/cout 기본sync+tie+‘\n’scanf/printfmmap
백준 10951 스타일50만 줄 입출력2100ms180ms140ms-
100만 정수 파일 읽기4MB 파일850ms120ms95ms25ms
1000만 줄 로그 파싱80MB 파일12초1.8초1.2초0.3초

버퍼 크기·접근 패턴별 성능

조건256B 버퍼64KB 버퍼mmap
64MB 순차 읽기420ms28ms25ms
랜덤 1000회 접근-180ms45ms

결론: 버퍼 64KB 이상 권장. mmap은 랜덤 접근에서 특히 유리합니다.


11. 프로덕션 패턴

패턴 1: RAII로 mmap 관리

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <memory>

struct MmapDeleter {
    size_t len;
    void operator()(void* p) const {
        if (p && p != MAP_FAILED) munmap(p, len);
    }
};

using MmapPtr = std::unique_ptr<void, MmapDeleter>;

MmapPtr map_file(const char* path, size_t& out_size) {
    int fd = open(path, O_RDONLY);
    if (fd < 0) return nullptr;
    struct stat st;
    if (fstat(fd, &st) < 0) { close(fd); return nullptr; }
    out_size = st.st_size;
    void* p = mmap(nullptr, out_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (p == MAP_FAILED) return nullptr;
    return MmapPtr(p, MmapDeleter{out_size});
}

패턴 2: I/O 풀 + io_uring

고성능 서버에서는 io_uring 인스턴스 풀을 스레드당 하나씩 두고, 각 스레드가 자신의 ring으로만 요청을 제출합니다. (lock-free)

// 의사 코드
thread_local io_uring tls_ring;
void init_thread() {
    io_uring_queue_init(1024, &tls_ring, 0);
}
void handle_request(int fd) {
    // tls_ring으로 비동기 read/write
}

패턴 3: 코테 vs 프로덕션 선택 가이드

상황권장
백준/프로그래머스sync+tie+‘\n’
로컬 대용량 파일 (수 GB)mmap
고성능 서버 (수만 연결)io_uring + epoll
단순 스크립트/유틸기본 cin/cout

패턴 4: 버퍼 풀 재사용

// io_uring에서 버퍼 풀 사용
struct BufferPool {
    std::vector<std::unique_ptr<char[]>> pool;
    std::vector<bool> in_use;

    int acquire() {
        for (size_t i = 0; i < in_use.size(); ++i) {
            if (!in_use[i]) { in_use[i] = true; return i; }
        }
        pool.push_back(std::make_unique<char[]>(4096));
        in_use.push_back(true);
        return pool.size() - 1;
    }
    void release(int idx) { in_use[idx] = false; }
};

패턴 5: 코테용 Fast I/O 헤더

// fast_io.hpp
#pragma once
#include <iostream>
struct FastIO {
    FastIO() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); }
} _fast_io;

#include "fast_io.hpp" 한 번으로 전역 최적화가 적용됩니다.

패턴 6: 대용량 파일 멀티스레드 파싱

mmap으로 매핑한 영역을 워커 스레드가 청크별로 나눠 파싱합니다. parse_chunkmap_file(패턴 1)로 얻은 포인터 구간을 처리합니다.

void process_large_file(const char* path, size_t size) {
    MmapPtr mapping = map_file(path, size);
    const char* p = static_cast<const char*>(mapping.get());
    size_t chunk = size / std::thread::hardware_concurrency();
    std::vector<std::thread> workers;
    for (size_t i = 0; i < std::thread::hardware_concurrency(); ++i) {
        size_t start = i * chunk, end = (i + 1 == std::thread::hardware_concurrency()) ? size : (i + 1) * chunk;
        workers.emplace_back([p, start, end]() { parse_chunk(p + start, p + end); });
    }
    for (auto& w : workers) w.join();
}

패턴 7: I/O 방식 선택 가이드

규모권장 방식
수만 줄 이하 (코테)sync+tie+‘\n’
수십만 줄 (코테 극한)scanf/printf 또는 getchar/putchar
수 MB~수 GB 파일mmap
고성능 서버 (수만 연결)io_uring + epoll

12. 백준/프로그래머스 템플릿

최소 템플릿

#include <iostream>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    // ...
    cout << answer << '\n';
    return 0;
}

여러 줄 출력할 때

for (int i = 0; i < n; ++i) {
    cout << result[i] << '\n';   // endl 쓰지 않기
}

한 줄에 공백으로 구분해 출력

cout << a << ' ' << b << ' ' << c << '\n';

입력이 많을 때

sync_with_stdio(false) + cin.tie(nullptr) 만 해도 대부분 해결됩니다. 그래도 부족하면:

  • scanf/printf를 쓰는 방법도 있습니다 (C 스트림은 별도로 버퍼링됨). 단, sync_with_stdio(false) 이후에는 cin/cout섞어 쓰지 마세요.

scanf/printf vs cin/cout 한눈에

항목cin / coutscanf / printf
속도기본 설정은 느림. 위 최적화 후에는 비슷한 편C 스트림, 보통 빠름
형식타입 안전, >>/<< 오버로드형식 문자열(%d, %lld 등) 직접 지정
섞기sync_with_stdio(false) 이후엔 C 스트림과 섞지 말 것같은 C 스트림이면 순서 보장
코테최적화 두 줄 + '\n' 쓰면 충분한 경우 많음극한 입력량에서 선택지

정리하면, cin/cout을 쓰면 형식 실수는 줄지만 기본 설정이 느리므로 반드시 sync_with_stdio(false)cin.tie(nullptr)를 쓰고, scanf/printf는 C 스트림만 쓸 때(즉 cin/cout과 섞지 않을 때) 대량 입출력에서 자주 씁니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 코딩테스트 입출력 | “시간 초과(TLE) 났어요” sync_with_stdio 복붙 템플릿
  • C++ 코딩테스트 팁 | “백준/프로그래머스” 합격하는 10가지 비법
  • C++ 파일 연산 완벽 가이드 | ifstream·바이너리 I/O·mmap·io_uring·원자적 쓰기까지

이 글에서 다루는 키워드 (관련 검색어)

C++ 입출력 최적화, 빠른 IO, mmap, io_uring, 백준 시간초과 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목권장
동기화ios_base::sync_with_stdio(false);
tiecin.tie(nullptr);
줄바꿈cout << x << '\n'; (endl 사용 지양)
사용 위치main() 맨 위, 한 번만
고급 (파일)mmap, io_uring (프로덕션)

endl이 느린 이유를 한 줄로 말하면: 매번 출력 버퍼를 비우기 때문이고, 버퍼를 비우는 것은 실제 I/O를 유발하므로 횟수가 많을수록 극도로 느려집니다.
백준·프로그래머스에서 C++로 코딩 테스트할 때는 위 세 가지(동기화 해제, tie 해제, '\n' 사용)를 습관화하면 시간 초과를 상당 부분 줄일 수 있습니다. 프로덕션 환경에서는 mmap·io_uring을 활용해 더 높은 성능을 얻을 수 있습니다.


구현 체크리스트

  • ios_base::sync_with_stdio(false) main 시작 시 호출
  • cin.tie(nullptr) 호출
  • endl 대신 '\n' 사용
  • sync_with_stdio(false) 후 cin/cout과 printf/scanf 섞지 않기
  • mmap 사용 시 RAII로 munmap 보장
  • io_uring 사용 시 완료 전 버퍼 해제 금지

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 코딩 테스트에서는 sync+tie+‘\n’ 세 가지가 필수입니다. 실무에서는 대용량 로그 처리, 파일 서버, DB 엔진 등에서 mmap·io_uring을 활용해 I/O 병목을 줄입니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreferenceliburing 문서를 참고하세요. io_uring은 Linux 커널 문서의 io_uring 설계 문서도 유용합니다.

한 줄 요약: sync_with_stdio(false)·tie(nullptr)·‘\n’으로 코테에서 TLE를 줄이고, 프로덕션에서는 mmap·io_uring으로 극한 성능을 뽑을 수 있습니다. 다음으로 문자열 파싱(#32-2)를 읽어보면 좋습니다.

다음 글: [C++ 코테 압축 #32-2] C++ 문자열(String) 처리 영혼까지 끌어모으기

이전 글: [C++ 실전 가이드 #31-3] 데이터베이스 연동: SQLite와 PostgreSQL


관련 글

  • C++ 문자열 파싱 완벽 가이드 | stringstream·getline·제로카피·성능 벤치마크
  • C++ 코테용 STL 컨테이너/알고리즘 시간복잡도 치트시트 [#32-3]
  • C++ 메모리 풀 완벽 가이드 | 객체 풀·슬랩·아레나·std::pmr 실전 [#32-2]
  • C++ 코딩테스트 입출력 |
  • C++ 코딩테스트 팁 |