본문 바로가기
TYLER SONGBlog
블로그 목록
CS학습

Branch Prediction: CPU가 분기 결과를 미리 추측해 파이프라인 정체를 없애는 기법

분기 예측(Branch Prediction)은 CPU가 조건 분기의 실제 결과가 계산되기 전에 다음에 실행할 명령어를 미리 추측해 파이프라인을 계속 채우는 하드웨어 기법이다. 예측이 맞으면 지연 없이 실행이 이어지지만, 틀리면 파이프라인을 비우고 다시 채우는 큰 비용이 발생한다. 정렬된 데이터에서 반복문이 빨라지는 현상, `__builtin_expect`

송민성9분 읽기

1. 개념

분기 예측(Branch Prediction)은 CPU가 if, for, while 같은 조건 분기 명령어(conditional branch)를 만났을 때, 조건 계산이 끝나기 전에 분기 결과(taken/not-taken)를 미리 추측하여 그에 맞는 명령어를 파이프라인에 미리 적재하는 하드웨어 기법이다.

현대 CPU는 명령어 하나를 여러 단계로 나눠 동시에 처리하는 파이프라이닝(pipelining)을 사용한다. 문제는 조건 분기 명령어가 파이프라인 앞쪽 단계에 들어왔을 때, 그 조건이 참인지 거짓인지는 훨씬 뒤 단계(실행 단계)에서야 확정된다는 점이다. 이 결과를 기다리면 파이프라인이 비게 되는데, 이를 제어 위험(control hazard)이라 부른다. 분기 예측은 이 대기 시간을 없애기 위한 해법이다.

2. 왜 사용하는가

파이프라인이 깊을수록(예: 현대 x86 CPU는 14~20단계 내외) 분기 결과를 기다릴 때 손해 보는 사이클 수가 커진다. 분기마다 결과가 나올 때까지 매번 멈춘다면, 프로그램의 분기 밀도(대략 명령어 5~6개당 1개꼴로 분기가 나온다고 알려져 있다)를 고려할 때 CPU 성능이 크게 떨어진다.

분기 예측이 없다면:

  • 매 조건 분기마다 실행 단계까지 기다린 후 다음 명령어를 가져와야 한다.
  • 파이프라인 깊이만큼의 사이클을 매번 낭비한다.

분기 예측이 있다면:

  • 예측이 맞을 경우(현대 CPU는 90~99% 정확도를 달성) 파이프라인이 끊기지 않고 계속 흐른다.
  • 예측이 틀린 경우에만 파이프라인 플러시(flush) 비용을 지불한다.

즉 분기 예측은 "대부분 맞는 도박"을 통해 평균 실행 속도를 크게 끌어올리는 트레이드오프다.

3. 동작 원리

3.1 정적 예측(Static Prediction)

초창기 방식으로, 하드웨어 상태 없이 고정된 규칙으로 예측한다. 예를 들어 "뒤로 가는 분기는 taken, 앞으로 가는 분기는 not-taken"으로 가정하는 방식(BTFN, Backward Taken Forward Not-taken)이 있다. 반복문의 끝에서 처음으로 되돌아가는 분기가 대부분 taken이라는 경험칙에 기반한다.

3.2 동적 예측(Dynamic Prediction)

실제 실행 이력을 저장해 예측 정확도를 높이는 방식이다.

분기 이력 테이블(BHT, Branch History Table): 분기 명령어의 주소(PC) 일부를 인덱스로 사용해, 해당 분기가 최근에 taken이었는지 not-taken이었는지를 기록한다.

2비트 포화 카운터(2-bit saturating counter): 단순히 직전 결과만 보면 반복문 마지막 반복에서 예측이 틀리는 문제가 생긴다. 이를 보완하기 위해 0~3 사이 값을 갖는 카운터를 사용한다.

text
00 (strongly not-taken) → 01 (weakly not-taken) → 10 (weakly taken) → 11 (strongly taken)

taken이면 카운터를 +1, not-taken이면 -1 하되 범위를 벗어나지 않는다. 값이 2 이상이면 taken으로 예측한다. 이 방식은 한 번의 예외적인 결과로 예측이 즉시 뒤집히는 것을 막아준다.

2단계 적응 예측기(Two-level Adaptive Predictor): Yeh & Patt가 제안한 구조로, 전역 또는 지역 분기 이력 레지스터(BHR, Branch History Register)와 패턴 이력 테이블(PHT, Pattern History Table)을 결합한다. 최근 N번의 분기 결과 패턴 자체를 인덱스로 사용해, 반복되는 패턴(예: taken-taken-not taken 반복)까지 학습한다. 현대 CPU(TAGE 예측기 등)는 이보다 훨씬 정교하게 여러 길이의 이력을 동시에 참조한다.

분기 목표 버퍼(BTB, Branch Target Buffer): 분기 결과(taken/not-taken)뿐 아니라 taken일 때 점프할 목표 주소까지 캐시해, 목표 주소 계산을 기다리지 않고 바로 명령어를 가져올 수 있게 한다.

반환 주소 스택(RAS, Return Address Stack): 함수 호출(call)의 반환 주소는 스택 구조로 예측하면 거의 100% 맞기 때문에 별도의 전용 예측기를 둔다.

3.3 예측 실패 시 처리(Misprediction Penalty)

예측이 틀리면 다음 과정이 일어난다.

  1. 잘못 가져와 실행 중이던 파이프라인 내 명령어들을 모두 무효화(squash)한다.
  2. 올바른 분기 목표로 프론트엔드(fetch 단계)를 다시 설정한다.
  3. 파이프라인을 처음부터 다시 채운다.

이 비용을 오예측 페널티(misprediction penalty)라 하며, 파이프라인 깊이와 비슷한 수준인 대략 15~20 사이클 정도가 현대 고성능 x86 코어에서 일반적으로 알려진 수치다(마이크로아키텍처마다 차이가 있다).

4. 코드 예제

분기 예측 효과를 체감하는 가장 유명한 예제는 "정렬된 배열을 순회하며 조건 분기를 실행하면 정렬 안 된 배열보다 빠르다"는 현상이다. 데이터가 정렬돼 있으면 분기 패턴이 규칙적(처음엔 계속 not-taken, 어느 지점부터 계속 taken)이라 예측기가 쉽게 학습하지만, 무작위 데이터는 패턴이 없어 예측이 자주 틀린다.

python
import random import time N = 300_000 data = [random.randint(0, 255) for _ in range(N)] threshold = 128 def sum_with_branch(arr, threshold): total = 0 for x in arr: if x >= threshold: # 조건 분기: CPU가 매 반복마다 예측 수행 total += x return total # 정렬 안 된 배열: 분기 결과가 무작위 -> 예측 실패 빈번 unsorted = data[:] start = time.perf_counter() sum_with_branch(unsorted, threshold) t_unsorted = time.perf_counter() - start # 정렬된 배열: 분기 결과가 규칙적 -> 예측 성공률 높음 sorted_data = sorted(data) start = time.perf_counter() sum_with_branch(sorted_data, threshold) t_sorted = time.perf_counter() - start print(f"unsorted: {t_unsorted:.4f}s") print(f"sorted: {t_sorted:.4f}s")

참고: CPython은 인터프리터 오버헤드가 커서 이 차이가 C로 작성했을 때보다 훨씬 덜 극적으로 나타난다(경우에 따라 거의 차이가 안 보일 수도 있다). 실제 하드웨어 분기 예측 효과를 정확히 측정하려면 하드웨어 성능 카운터를 직접 확인하는 것이 정확하다.

shell
# Linux perf로 실제 분기 오예측 횟수 측정 (컴파일된 네이티브 바이너리 대상 권장) perf stat -e branches,branch-misses ./your_program # 출력 예시 해석 # branches: 1,234,567,890 # branch-misses: 12,345,678 (약 1% 오예측률)

5. 시간 복잡도 또는 성능 특성

분기 예측은 알고리즘 시간복잡도(Big-O)와는 다른 층위의 개념이다. Big-O는 연산 횟수의 점근적 증가율을 다루지만, 분기 예측은 동일한 연산 횟수 안에서 실제 소요 사이클(clock cycle)에 영향을 준다.

  • 예측 성공 시: 추가 지연 없음(0 사이클 페널티)
  • 예측 실패 시: 파이프라인 플러시로 약 15~20 사이클 손실(마이크로아키텍처에 따라 다름)
  • 현대 데스크톱/서버 CPU의 분기 예측 정확도: 일반적인 프로그램 기준 90% 후반 % 수준(TAGE 계열 예측기 기준, 벤치마크와 워크로드에 따라 편차 큼)
  • 예측 실패율이 1%p만 늘어도 분기 밀도가 높은 코드(파서, 인터프리터, 트리 순회 등)에서는 체감 가능한 성능 저하가 발생한다.

6. 실무 사용 사례

컴파일러 힌트: GCC/Clang의 __builtin_expect(x, 1) (C/C++) 또는 C++20의 [[likely]], [[unlikely]] 속성은 컴파일러가 분기의 예상 결과를 코드 배치에 반영하도록 유도한다. 다만 이는 정적 예측 단계의 힌트일 뿐, 최종 예측은 여전히 런타임 동적 예측기가 수행한다.

JIT 컴파일러: V8, JVM HotSpot 같은 JIT은 실행 중 수집한 분기 통계(profile-guided)를 바탕으로 자주 taken되는 경로를 직선 코드(fast path)로 배치하고, 드문 경로는 별도 블록으로 분리해(cold path) 코드 지역성과 예측 정확도를 함께 높인다.

분기 없는 코드(Branchless Programming): 조건 분기 대신 비트 연산이나 cmov(conditional move) 같은 조건부 명령어로 대체해 예측 실패 리스크 자체를 없애는 기법이다. 정렬 알고리즘, 이진 탐색(binary search)의 저수준 구현, 암호화 코드(타이밍 사이드채널 방지 목적)에서 자주 쓰인다.

데이터베이스 쿼리 엔진: 컬럼형 DB의 필터링 연산(예: WHERE age > 30)을 벡터화(SIMD)하거나 분기 없는 방식으로 구현해 대량 스캔 시 예측 실패 비용을 회피한다.

7. 주의할 점

추측성 최적화 금지: 분기 예측 효과는 데이터 분포와 CPU 마이크로아키텍처에 따라 달라진다. "정렬하면 빠르다"는 규칙을 모든 상황에 적용하지 말고, 실제로는 perf, vtune 같은 도구로 branch-misses 카운터를 측정한 뒤 최적화 여부를 판단해야 한다.

정렬 비용과의 트레이드오프: 분기 예측 실패를 줄이려고 데이터를 정렬하면 정렬 자체에 O(n log n) 비용이 든다. 이 비용이 얻는 이득보다 클 수 있다.

보안 문제(Spectre): 투기적 실행(speculative execution)은 분기 예측이 틀렸을 때 실행됐던 명령어의 부작용이 캐시 상태 등에 흔적을 남길 수 있다. Spectre류 취약점은 이 흔적을 사이드채널로 이용해 권한 없는 메모리를 유추하는 공격이다. 이 때문에 커널과 브라우저 엔진은 특정 분기 예측 경로를 명시적으로 차단하는 완화 기법(예: 리타폴린, retpoline)을 도입했다.

과도한 저수준 최적화 지양: 대부분의 웹/애플리케이션 개발 환경에서는 언어 런타임, 가비지 컬렉터, I/O 대기 시간 등이 분기 예측 미스보다 훨씬 큰 성능 요인이다. 분기 예측 최적화는 HPC, 게임 엔진, 데이터베이스 내부 엔진처럼 사이클 단위 성능이 중요한 영역에서 의미가 크다.

8. 핵심 정리

  • 분기 예측은 파이프라인 CPU가 조건 분기 결과를 미리 추측해 제어 위험(control hazard)으로 인한 정체를 없애는 하드웨어 기법이다.
  • 정적 예측(고정 규칙)과 동적 예측(2비트 포화 카운터, 2단계 적응 예측기, TAGE 등 실행 이력 기반)으로 나뉜다.
  • 예측 성공 시 비용은 0에 가깝지만, 실패 시 파이프라인 플러시로 약 15~20 사이클을 잃는다.
  • 정렬된 데이터에서 조건 분기 루프가 빨라지는 현상은 분기 패턴이 규칙적이라 예측기가 잘 맞히기 때문이다.
  • [[likely]]/__builtin_expect, JIT 프로파일 기반 최적화, 분기 없는 코드(branchless code)는 모두 이 원리를 실무에 적용한 사례다.
  • 투기적 실행의 부산물은 Spectre 같은 보안 취약점으로 이어질 수 있어, 성능과 보안 사이의 균형을 함께 고려해야 한다.
© 2026 Tyler Song