이번 문서의 목표: 이 파일을 다 읽으면 ARM 파이프라인이 왜 필요하고 어떻게 동작하는지 단계별로 설명할 수 있고, 파이프라인 때문에 생기는 “PC가 실제보다 앞서 보이는” 함정을 계산으로 풀 수 있으며, 인터럽트 지연(interrupt latency)을 구성 요소별로 나눠 실제 사이클 수로 계산할 수 있다.
왜 명령어를 한 번에 하나씩 끝내지 않고 파이프라인으로 처리할까
05편까지는 예외가 발생했을 때 CPU 내부에서 레지스터와 모드가 어떻게 바뀌는지를 다뤘다. 그런데 “CPU가 명령어를 처리한다”는 것 자체가 실제로는 인출(fetch)→해독(decode)→실행(execute)이라는 여러 단계를 거치는 일이다. 만약 한 명령어가 이 세 단계를 모두 마친 뒤에야 다음 명령어의 인출을 시작한다면, 매 명령어마다 세 단계의 시간을 온전히 다 써야 하므로 처리량이 낮다.
파이프라인(pipeline)은 공장의 조립 라인처럼, 한 명령어가 해독 단계에 있는 동안 다음 명령어를 이미 인출해 두는 방식으로 여러 명령어의 처리 단계를 겹쳐 실행하는 기법이다. 이렇게 하면 각 단계를 담당하는 하드웨어가 매 사이클 쉬지 않고 일하게 되어, 전체 처리량이 크게 늘어난다.
ARM의 3단계 파이프라인
쉽게 말하면: ARM7TDMI와 Cortex-M 계열은 기본적으로 인출-해독-실행 세 단계를 겹쳐서 처리하는 3단계 파이프라인을 쓴다.
가장 단순한 형태인 ARM7TDMI(및 Cortex-M0 계열)의 파이프라인은 다음 3단계로 구성된다.
| 단계 | 이름 | 하는 일 |
|---|---|---|
| 1 | 인출(Fetch) | 메모리에서 명령어를 읽어온다 |
| 2 | 해독(Decode) | 읽어온 명령어가 무슨 연산인지 해석하고 레지스터를 읽는다 |
| 3 | 실행(Execute) | ALU 연산·메모리 접근·레지스터 기록 등 실제 동작을 수행한다 |
| 클록 사이클 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 명령어 1 | 인출 | 해독 | 실행 | ||
| 명령어 2 | 인출 | 해독 | 실행 | ||
| 명령어 3 | 인출 | 해독 | 실행 |
이 표에서 보듯, 명령어 1이 실행 단계에 있는 시점(사이클 3)에 명령어 2는 이미 해독 단계, 명령어 3은 인출 단계에 들어가 있다. 세 개의 명령어가 동시에 파이프라인 안에서 서로 다른 단계를 밟고 있는 것이다. 이렇게 겹쳐 처리한 덕분에, 파이프라인이 가득 찬 이후에는 매 사이클마다 명령어 하나씩 완료되는 것처럼 보인다(이상적인 경우 기준이며, 실제로는 뒤에서 다룰 분기 등으로 지연이 생긴다).
Cortex-M4처럼 더 발전한 코어는 3단계보다 세분화된 파이프라인을 쓰기도 하지만, 독학사 시험에서 다루는 핵심은 “왜 파이프라인이 필요한가”와 “파이프라인 때문에 생기는 함정 두 가지”이므로, 이 문서는 그 두 함정 — PC 오프셋과 분기 패널티 — 을 집중적으로 다룬다.
함정 1: 파이프라인 때문에 PC가 실제보다 앞서 보인다
쉽게 말하면: 명령어 하나가 실행 단계에 있을 때, 그 명령어를 기준으로 한 PC 값은 이미 두 명령어 뒤(8바이트 앞선 주소)를 가리키고 있다.
이것이 클래식 ARM(ARM7TDMI, ARMv4T 기반)에서 가장 유명한 시험 함정이다. 3단계 파이프라인이 항상 세 개의 명령어를 동시에 처리하고 있다는 것은, 어떤 명령어가 실행 단계에 있는 바로 그 순간에 PC는 이미 그 명령어의 주소보다 명령어 2개만큼 앞선 주소(인출 단계에 있는 명령어의 주소)를 가리키고 있다는 뜻이다.
ARM 명령어는 32비트(4바이트) 고정 길이이므로, 이 어긋남은 정확히 다음과 같다.
- : 프로그램 카운터(Program Counter)의 현재 값
- : 명령어 2개 앞선 만큼의 바이트 오프셋(명령어 1개 = 4바이트 × 2단계 앞섬)
예를 들어 주소 0x1000에 있는 명령어가 지금 막 실행 단계에 들어갔다면, 그 순간 PC 레지스터의 실제 값은 0x1000 + 8 = 0x1008이다. 이 어긋남을 몰라서 생기는 실제 문제가 있다. MOV PC, r0처럼 현재 PC 값을 읽어서 다른 레지스터에 저장한 뒤, 그 값을 기준으로 분기 주소를 계산하는 코드를 짤 때, “지금 실행 중인 이 명령어의 주소”라고 착각하고 오프셋을 계산하면 실제로는 8만큼 어긋난 주소로 튀게 된다.
시험 함정: 이 +8 오프셋은 ARM 상태(32비트 명령어)일 때의 값이다. Thumb 상태(16비트 명령어)에서는 명령어 2개 앞섬이 바이트이므로 오프셋이 +4가 된다. “PC는 항상 +8”이라고 암기하면 Thumb 모드 문제에서 틀린다.
이 때문에 05편에서 다룬 예외 진입 시 LR에 저장하는 “복귀 주소”도 예외 종류에 따라 서로 다른 보정값을 뺀다. 예를 들어 IRQ 예외가 발생하면 하드웨어는 LR에 현재 PC(인출 중이던 명령어 주소) - 4를 저장하는데, 이는 인터럽트가 발생한 시점에 실행이 끝나지 못한 명령어로 정확히 되돌아가기 위해 파이프라인 어긋남만큼을 미리 보정해 둔 값이다.
함정 2: 분기 명령은 파이프라인을 비운다(파이프라인 플러시)
쉽게 말하면: 분기(branch) 명령이 실행되면 이미 인출·해독해 둔 다음 명령어들이 전부 쓸모없어져 버려지고, 파이프라인을 처음부터 다시 채워야 한다.
파이프라인은 “다음 명령어는 현재 명령어 바로 다음 주소에 있다”는 가정 아래 미리 인출·해독을 진행한다. 그런데 분기(branch) 명령어가 실행되면 이 가정이 깨진다. 분기 목적지 주소는 실행 단계에 가서야 확정되는데, 그 시점에는 이미 잘못된(분기 이전 순서를 따라간) 명령어 두 개가 인출·해독 단계에 들어가 있는 상태다. 이 잘못 들어간 명령어들은 실행되면 안 되므로 파이프라인에서 버려지고(파이프라인 플러시, pipeline flush), CPU는 새로 계산된 분기 목적지 주소부터 인출을 다시 시작해야 한다.
3단계 파이프라인 기준으로, 분기가 성공하면 보통 2사이클의 패널티(penalty, 지연)가 발생한다. 잘못 인출·해독된 두 단계만큼을 다시 채워야 하기 때문이다. 이 패널티 때문에 조건 분기가 많은 코드는 파이프라인이 없는 이상적인 경우보다 실제 실행 시간이 늘어난다.
시험 함정: 분기 패널티는 파이프라인 단계 수와 직결된다. 3단계 파이프라인이면 2사이클, 더 깊은 파이프라인(5단계, 8단계 등)일수록 잘못 채워진 단계가 많아 패널티도 커진다. “파이프라인이 깊을수록 항상 빠르다”는 생각은 분기가 잦은 코드에서는 틀릴 수 있다.
인터럽트 지연이란 무엇인가
쉽게 말하면: 인터럽트 지연은 “인터럽트가 발생한 순간”부터 “그 인터럽트를 처리하는 첫 명령어가 실제로 실행되는 순간”까지 걸리는 시간이다.
인터럽트 지연(interrupt latency)은 외부 장치가 인터럽트 요청 신호를 보낸 시각부터, CPU가 그 요청에 대응하는 인터럽트 서비스 루틴(ISR, Interrupt Service Routine)의 첫 번째 명령어를 실제로 실행하기 시작하는 시각까지의 시간을 말한다. 실시간 시스템(13편에서 자세히 다룬다)에서는 이 지연이 얼마나 짧고 예측 가능한지가 시스템의 신뢰도를 좌우하므로, 독학사 시험에서도 지연을 구성하는 요소를 나눠 계산하는 문제가 자주 나온다.
인터럽트 지연은 크게 다음 세 요소의 합으로 계산할 수 있다.
- : 전체 인터럽트 지연 시간
- : 인터럽트 요청이 들어온 시점에 이미 실행 중이던 명령어(또는 명령어 그룹)를 마무리하는 데 걸리는 시간
- : 예외 진입 시 레지스터(또는 뱅킹된 레지스터, Cortex-M의 자동 스택 프레임)를 저장하는 데 걸리는 시간
- : 벡터 테이블을 읽어 ISR의 시작 주소를 확정하고 그 위치에서 명령어를 인출하는 데 걸리는 시간
클래식 ARM 기준 예시 계산
클록 주파수가 50메가헤르츠(MHz)인 ARM7TDMI 코어에서, 다음 조건으로 인터럽트 지연을 계산해 보자.
- 인터럽트 요청 시점에 실행 중이던 명령어가 완료될 때까지 최대 3사이클이 걸린다고 가정한다(가장 긴 메모리 접근 명령어 기준).
- 예외 진입 시 LR·SPSR 저장과 모드 전환에 2사이클이 걸린다.
- 벡터 테이블에서 목적지 주소를 읽고 ISR의 첫 명령어를 인출하는 데 2사이클이 걸린다.
클록 주파수가 50MHz이므로 1사이클의 시간은 다음과 같다.
- : 클록 한 번이 걸리는 시간(주기)
- : 50MHz를 헤르츠 단위로 풀어 쓴 값(초당 클록 횟수)
즉 이 조건에서는 인터럽트가 발생한 뒤 ISR의 첫 명령어가 실행되기까지 140나노초가 걸린다. 이 계산에서 핵심은 “완료 대기 시간”이 고정값이 아니라 인터럽트가 언제 들어오느냐에 따라 달라지는 값이라는 점이다. 만약 인터럽트가 하필 가장 긴 명령어(예: 여러 레지스터를 한 번에 적재하는 LDM)가 막 시작된 직후에 들어온다면, 그 명령어가 끝날 때까지 기다려야 하므로 가 늘어난다.
시험 함정: 인터럽트 지연의 최댓값은 “가장 긴 명령어가 실행되는 도중에 인터럽트가 들어온 경우”를 기준으로 계산한다. 평균적인 명령어 실행 시간으로 계산하면 최악의 경우(worst case)를 놓치게 되는데, 실시간 시스템 설계에서는 평균이 아니라 최악의 경우 지연이 기준이 된다.
Cortex-M의 인터럽트 지연: 고정 지연과 테일 체이닝
Cortex-M 계열은 인터럽트 응답 속도를 예측 가능하게 만들기 위해 하드웨어 차원에서 지연을 최소화하고 고정했다. 대표적으로 Cortex-M3/M4는 다음과 같은 특징을 갖는다(참고자료의 Cortex-M4 TRM 참고).
- 고정된 최단 지연: 대기 상태(wait state) 없는 메모리 조건에서, 인터럽트 발생부터 ISR 첫 명령어 실행까지 12사이클로 고정되어 있다. 05편에서 다룬 자동 스택 저장(R0~R3, R12, LR, PC, xPSR을 하드웨어가 자동으로 push)이 이 12사이클 안에 포함된다.
- 테일 체이닝(tail-chaining): 하나의 ISR 처리가 끝나자마자 대기 중인 다음 인터럽트가 있으면, 스택을 다시 팝(pop)했다가 곧바로 또 푸시(push)하는 낭비 없이 바로 다음 ISR로 넘어간다. 이 경우 지연이 12사이클보다 훨씬 짧아진다(보통 6사이클 수준).
- 레이트 어라이벌(late-arrival): 현재 예외 처리 진입 절차가 진행되는 도중에 더 우선순위 높은 인터럽트가 새로 들어오면, 이미 시작한 스택 저장 절차를 그대로 활용하면서 목적지만 새 인터럽트의 ISR로 바꿔치기해 처리한다.
- : 대기 상태 없는 조건에서 고정된 지연 사이클 수
- : 코어 클록 한 사이클의 시간
만약 이 Cortex-M 코어가 100MHz로 동작한다면 이므로, 인터럽트 지연은 나노초로 고정된다. 클래식 ARM과 달리 “실행 중이던 명령어가 언제 끝나는지”에 따라 크게 흔들리지 않는다는 점이 실시간 응답성 측면에서 중요한 차이다.
자주 틀리는 점
- 파이프라인의 PC 오프셋(+8 또는 +4)을 “명령어가 실행되는 주소”와 혼동해, 분기 주소 계산이나 예외 복귀 주소 계산에서 부호나 크기를 반대로 적용하는 실수가 흔하다.
- 분기 패널티를 “항상 고정된 절대 사이클 수”로 암기하고 파이프라인 단계 수가 다른 코어에도 그대로 적용하는 오류가 있다. 패널티는 파이프라인 깊이에 따라 달라진다.
- 인터럽트 지연 계산에서 벡터 테이블 인출 시간이나 레지스터 저장 시간 중 하나를 빠뜨리고 “명령어 완료 대기 시간”만으로 답을 내는 경우가 많다. 세 요소를 모두 더해야 한다.
핵심 정리
- ARM은 인출-해독-실행 3단계 파이프라인으로 여러 명령어의 처리 단계를 겹쳐 실행해 처리량을 높인다.
- 파이프라인 때문에 실행 중인 명령어 기준 PC 값은 ARM 상태에서 +8, Thumb 상태에서 +4만큼 앞서 보인다.
- 분기 명령어는 잘못 인출된 명령어를 버리는 파이프라인 플러시를 일으키며, 3단계 파이프라인 기준 2사이클의 분기 패널티가 발생한다.
- 인터럽트 지연은 (실행 중이던 명령어 완료 시간) + (레지스터 저장 시간) + (벡터 인출 시간)의 합이며, 실시간 시스템에서는 최악의 경우 지연을 기준으로 설계한다.
- Cortex-M은 대기 상태 없는 조건에서 12사이클의 고정 지연을 보장하며, 테일 체이닝과 레이트 어라이벌로 연속·긴급 인터럽트 처리를 최적화한다.