비순차 실행
CPU가 명령어를 적힌 순서대로만 실행하면, 앞 명령어 하나가 오래 걸릴 때 뒤의 명령어가 모두 함께 멈춥니다. 대표적인 경우가 캐시 미스입니다. 메모리에서 데이터를 가져오는 동안 수십에서 수백 사이클이 흐르는데, 그 사이 뒤쪽에 그 데이터와 상관없는 계산이 있어도 순차 실행 CPU는 기다릴 수밖에 없습니다. 비순차 실행(Out-of-Order Execution)은 명령어 사이의 데이터 의존성을 하드웨어가 추적해, 피연산자가 준비된 명령어부터 먼저 실행합니다. 실행 순서는 뒤섞이지만 결과는 리오더 버퍼(ROB)를 거쳐 원래 프로그램 순서대로 확정되므로, 프로그램 입장에서는 순서대로 실행한 것과 똑같이 보입니다.
스마트폰부터 서버까지 오늘날 대부분의 고성능 CPU 코어가 이 방식을 씁니다. 1964년 CDC 6600의 스코어보드가 동적 스케줄링을 처음 선보였고, 1967년 IBM System/360 Model 91의 부동소수점 장치에 쓰인 토마술로 알고리즘이 하드웨어 레지스터 리네이밍을 더해 오늘날 비순차 실행의 뿌리가 됐습니다. 결과를 순서대로 확정하는 리오더 버퍼는 1985년 정밀 예외를 위해 제안됐고, 1990년대 중반 이후 범용 프로세서로 널리 퍼졌습니다. 컴파일러가 명령어 순서를 미리 바꿔 두는 정적 스케줄링과 달리, 비순차 실행은 프로그램이 돌아가는 중에 실제로 어떤 데이터가 늦게 도착하는지 보고 순서를 정하는 동적 스케줄링입니다.
핵심 개념
명령어 수준 병렬성(ILP)
서로 결과를 주고받지 않는 명령어는 동시에 실행하거나 순서를 바꿔 실행해도 결과가 같습니다. 프로그램 안에 이런 독립 명령어가 얼마나 있는지가 비순차 실행으로 얻을 수 있는 성능의 상한입니다.
핵심 개념
진짜 의존성(RAW)
앞 명령어가 쓴 레지스터를 뒤 명령어가 읽는 관계(Read After Write)입니다. 값이 만들어지기 전에는 계산할 수 없으므로 비순차 실행으로도 없앨 수 없고, 값이 나올 때까지 기다려야 합니다.
핵심 개념
가짜 의존성과 레지스터 리네이밍
WAR(Write After Read)과 WAW(Write After Write)는 값을 주고받아서가 아니라 같은 레지스터 이름을 다시 써서 생기는 충돌입니다. CPU는 프로그램의 레지스터 이름을 내부의 더 많은 물리 레지스터에 매번 새로 연결해(리네이밍) 이 충돌을 없앱니다.
핵심 개념
예약 스테이션(발행 대기열)
해독된 명령어는 실행 유닛 앞의 대기열에서 피연산자를 기다립니다. 필요한 값이 모두 준비되면 원래 순서와 상관없이 비어 있는 실행 유닛으로 발행(issue)됩니다. 교재에 따라 이 단계를 dispatch라 부르고, 대기열에 넣는 단계를 issue라 부르기도 합니다(Hennessy & Patterson의 토마술로 설명).
핵심 개념
리오더 버퍼(ROB)
명령어가 들어온 순서대로 한 칸씩 자리를 잡는 원형 버퍼입니다. 실행이 먼저 끝난 명령어도 결과를 여기에 보관한 채, 앞선 명령어가 모두 끝나기를 기다립니다.
핵심 개념
순서대로 확정과 정밀 예외
ROB 맨 앞(head)의 명령어가 완료돼야 그 결과를 레지스터와 메모리에 반영(commit, retire)합니다. 그래서 예외가 나면 그 지점 이전 명령어의 결과만 반영된 깔끔한 상태로 멈출 수 있습니다(정밀 예외).
비순차 실행의 역사
비순차 실행의 핵심 아이디어는 1967년 IBM System/360 Model 91의 부동소수점 장치에서 로버트 토마술로(Robert Tomasulo)가 쓴 알고리즘에서 나온 것으로 알려져 있습니다. 순서대로 확정하는 구조(리오더 버퍼)는 스미스(Smith)와 플레스쿤(Pleszkun)이 정밀 예외(논문 용어로 precise interrupt)를 위한 여러 해법 중 하나로 다룬 연구(1985년 ISCA)로 소개됩니다. 이후 일반 프로세서의 표준 기법이 되었습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
비순차 실행은 어떤 문제를 푸나요?
캐시 미스 같은 긴 지연 동안 뒤에 있는 독립 명령어를 먼저 실행해 파이프라인이 멈추는 시간을 줄입니다. 의존성이 없는 명령어의 순서를 바꿔도 결과가 같다는 점을 이용합니다.
면접 질문
리오더 버퍼(ROB)는 왜 필요한가요?
실행은 뒤섞여도 결과는 프로그램 순서대로 확정해야 합니다. ROB는 실행이 끝난 결과를 보관하다 맨 앞부터 순서대로 반영해 정밀 예외와 분기 예측 실패 복구를 가능하게 합니다.
면접 질문
레지스터 리네이밍이 없으면 어떻게 되나요?
WAR와 WAW 같은 가짜 의존성 때문에 독립적인 명령어도 같은 레지스터 이름을 쓰면 순서를 지켜야 해서 병렬성이 줄어듭니다.
면접 질문
슈퍼스칼라와 비순차 실행은 같은 말인가요?
다릅니다. 슈퍼스칼라는 한 사이클에 여러 명령어를 발행하는 폭에 대한 개념이고, 비순차 실행은 발행 순서를 프로그램 순서와 다르게 하는 개념입니다. 순차 실행하는 이중 발행 코어(예: Arm Cortex-A53)도 있습니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
의존성 사슬 끊기 — 누산기 나누기
// 누산기 하나: 매 덧셈이 직전 덧셈의 결과를 기다림 (하나의 긴 의존성 사슬) double sum = 0; for (int i = 0; i < n; i++) sum += a[i]; // 누산기 넷: 서로 독립인 사슬 4개라 덧셈 여러 개가 겹쳐 진행될 수 있음 double s0 = 0, s1 = 0, s2 = 0, s3 = 0; int i = 0; for (; i + 3 < n; i += 4) { s0 += a[i]; s1 += a[i + 1]; s2 += a[i + 2]; s3 += a[i + 3]; } for (; i < n; i++) s0 += a[i]; // 4로 나누어떨어지지 않는 나머지 double total = s0 + s1 + s2 + s3;
부동소수점 덧셈은 결과가 나오기까지 여러 사이클이 걸립니다. 누산기가 하나면 매 반복이 직전 덧셈을 기다려야 해서 비순차 실행이 앞당길 명령어가 없습니다. 누산기를 넷으로 나누면 독립된 사슬이 4개가 되어 덧셈이 겹쳐 진행됩니다. 단, 부동소수점은 더하는 순서에 따라 결과가 아주 조금 달라질 수 있어서 컴파일러가 이렇게 자동으로 바꾸지 않는 경우가 많습니다.
메모리 지연을 숨길 수 있는 코드와 없는 코드
// 배열: 다음 원소의 주소를 이전 로드 결과 없이 계산할 수 있음 → 여러 로드를 겹쳐 실행 long total = 0; for (int i = 0; i < n; i++) total += values[i]; // 연결 리스트: 다음 노드 주소가 이번 로드의 결과(p.next)에 들어 있음 → 로드끼리 사슬 for (Node p = head; p != null; p = p.next) total += p.value;
배열은 i번째 주소를 계산하는 데 앞선 로드의 결과가 필요 없어서, CPU가 캐시 미스 여러 개를 동시에 기다릴 수 있습니다(하드웨어 프리페치도 잘 동작합니다). 연결 리스트는 p.next를 읽어야 다음 주소를 알 수 있으므로 캐시 미스가 하나씩 차례로 일어나고, 비순차 실행도 그 사이에 할 일을 찾지 못합니다. 같은 개수의 원소를 더해도 리스트가 크게 느려질 수 있는 이유입니다.
레지스터 리네이밍 — 가짜 의존성 없애기
// 원래 명령어: r1이라는 이름을 두 번 사용 I1: r1 ← [a] // 캐시 미스로 오래 걸림 I2: r2 ← r1 + 1 // I1의 결과를 읽음 (진짜 의존, RAW) I3: r1 ← r5 × r6 // r1을 다시 씀: I2가 옛 r1을 읽기 전에 덮으면 안 됨 (WAR), I1과는 WAW I4: r7 ← r1 - r8 // I3이 쓴 새 r1을 읽음 (진짜 의존, RAW) // 리네이밍 후: 같은 r1이라도 쓸 때마다 다른 물리 레지스터(p1, p9)를 배정 I1: p1 ← [a] I2: p2 ← p1 + 1 I3: p9 ← p5 × p6 // 이름이 달라져 I1, I2를 기다릴 필요가 없음 I4: p10 ← p9 - p8
프로그램에서 쓸 수 있는 레지스터 이름은 몇 개 되지 않아서 같은 이름이 계속 재사용됩니다. 리네이밍이 없으면 I3은 I2가 옛 r1을 읽을 때까지 기다려야 하지만, 새 물리 레지스터 p9에 쓰면 I1의 캐시 미스와 상관없이 바로 실행할 수 있습니다. 진짜 의존성(I1 → I2, I3 → I4)은 그대로 남습니다.
Q1
비순차 실행(Out-of-Order Execution)의 핵심 아이디어는?
Q2
비순차 실행 CPU에서도 프로그램 결과가 순차 실행과 같게 보이도록 보장하는 장치는?
Q3
다음 중 앞선 명령어보다 먼저 실행할 수 없는 경우는?