- 상위 문서 : CPU
1 명령 주기
파일:Attachment/CPU/basic-instruction-processing-cycle.jpg
간단화 한 CPU Instruction Cycle(명령 주기)
파일:Attachment/CPU/basic-instruction-processing-cycle-with-exseption-hangling.jpg
예외처리를 추가한 간단한 CPU Instruction Cycle(명령주기)
기본 구성으로는 레지스터[1], 프로그램카운터[2], 명령어 레지스터 [3], 산술논리연산장치(ALU: arithmetic logic unit)[4], 제어부[5]와 내부 버스 등이 있다. 그 외에도 캐시 같은 부가 장치도 들어가 있는 경우가 대다수.
2 설명
거의 모든 종류의 CPU가 하는 일은 요약해보면 대부분 고작 아래 4기능이 전부다.
- Fetch(인출) : 메모리상의 프로그램 카운터가 가리키는 명령어를 CPU로 인출하여 적재.
- Decode(해석) : 명령어의 해석. 이 단계에서 명령어의 종류와 타겟 등을 판단한다.
- Execute(실행) : 해석된 명령어에 따라 데이터에 대한 연산을 수행한다.
- Writeback(쓰기) : 명령어대로 처리 완료된 데이터를 메모리에 기록한다.
가장 어려운 부분을 스킵해서 쉬워보이는 건 기분탓
위의 CPU는 아주 초급적인 교과서적인 설명에 기반하며, 어떤 상용 CPU도 저런 구조를 가지지 않는다. 그나마 명령어의 실행 순서를 섞을 수 없는 Inorder CPU[6]를 엄청나게 단순화하면 저런 설명이 가능하다고 우길 수도 있으려나? 여튼, 명령어의 실행 순서를 섞을 수 있는 현대의 Out-of-order CPU 들은 아래와 같은 순서를 거쳐 명령어를 실행한다.
- Fetch: 실행할 명령어들을 가져온다. 이후 단계들도 그렇지만, 한 번에 보통 4개 정도를 처리하는데, Superscalar라고 불리는 기술은 이렇게 한 사이클에 여러 명령어를 처리하는 것을 말한다.
- Decode: 이후 처리를 돕기 위해 명령어의 종류를 구분한다. Intel의 x86 ISA같이 복잡한 명령어를 쓰는 CISC의 경우, 내부적으로 RISC 명령어들로 쪼개지는 과정[7]이 여기서 수행된다. 옛날 프로그램을 짜기 어려웠던 이유는 바로 이 디코드 동작시 불러오는 CPU 명령함수들이 CPU마다 전부 다 다르다는 것 때문이었다. 현재는 몇 개 회사가 CPU 공급을 독점하면서 그나마 단순화되었다. (같은 회사는 추가 기능이 없다면 같은 명령함수를 쓰는 게 일반적)
- Rename: 명령어가 가리키는 레지스터(CPU에서 값을 저장하는 x86 ISA의 eax나 ebx등의 이름있는 공간들)를, 내부에 숨어있는 물리적 레지스터로 매핑한다. 이러한 과정은 Out-of-order CPU에서 발생하는 False-dependency[8] 문제를 해결하기 위해 필수적이다.
- Dispatch: 명령어가 실행하기 위해 기다리는 대기열 (ROB[9], IQ[10], LSQ[11])에 명령어를 넣는다.
- Issue: 대기열에 있는 명령어가 실행될 수 있으면[12] 실행하기 위한 장치(가령 계산 명령어는 ALU, 메모리 명령어는 Cache)로 보낸다. 참고로 프로그램에서 시간상 뒤의 명령어가 앞의 명령어보다 먼저 Issue될 수 있다. 이것이 바로 Out-of-order CPU의 핵심 동작 중 하나다.
- Execute: 실행한다
더 설명이 필요한지? - Writeback: 결과값을 레지스터에 써야 한다면 쓴다. 결과값을 기다리고 있던 명령어가 있다면 결과가 생겼다고 알려준다.
- Commit: 명령어 수행을 완료하고, 명령어 실행을 위해 할당받은 자원을 모두 토해낸다. 명령어의 실행 결과를 사용자에게 노출시키며 (이거 전에는 노출이 안 된다), 이후로는 명령어의 실행을 취소[13] 할 수 없다.
그리고 Out-of-order CPU를 만들기 위해선, 위의 명령어 처리 과정 외에도 몇몇 핵심 기술들이 요구된다.
- Cache (캐시 메모리): 주 메모리 값을 임시 저장하는 작고 빠른 메모리. 주 메모리 접근은 CPU입장에서 100사이클 정도 놀게 만드는 엄청나게 느린 동작이므로 Cache가 없으면 명령어를 빠르게 실행하고 싶어도 값이 준비가 안 돼서 불가능하다. 캐시 메모리도 한 개만 존재하는 것이 아니라, 보통 중요도와 접근 빈도에 따라 L1~L3의 3개 캐시 메모리를 사용하며, 숫자가 작을수록 용량이 더 작고, 더 빠르다. 또한 캐시 메모리만을 위한 색인 제조 방법과 구동방식은 해당 문서에서 확인 바람.
- Branch prediction: Fetch를 매 사이클마다 하기 위해선 다음에 실행할 명령어 주소를 알아야 하는데, branch 명령의 결과가 나오려면 꽤나 시간이 걸린다 (게다가 프로그램마다 차이는 있지만, 명령어의 약 30%정도는 branch이다.) 그러니 branch 명령의 결과를 과거의 기록을 기반으로 예측하는 기술이 필요하다.
- Speculative memory disambiguation: 메모리 접근 명령어들은 메모리 접근 전에 접근할 메모리 주소를 먼저 구해야 한다. 이 때문에 만약 LSQ에 Store 명령이 접근할 메모리 주소를 계산하기 위해 대기중이면, 이후 모든 Load 명령어는 접근할 메모리 주소가 준비되었다 하더라도 메모리 접근을 할 수 없다. 혹시라도 그 Store가 접근하는 주소가 Load가 접근하는 주소랑 같으면 가장 신선한 값은 메모리가 아니라 그 Store 명령어가 가지고 있기 때문이다.[14] 하지만 할 수 없는 걸 그냥 해버리면? (즉, Speculative memory disambiguation)
역시 Out-of-order CPU! Inorder CPU가 못하는 걸 태연하게 해버려! 거기에 동경하게 돼대부분의 경우 접근하는 메모리 주소는 다르기 때문에 문제가 없으며, 문제가 있더라도 그 Store가 접근하는 메모리 주소를 계산하기 전까진 Load들을 Commit을 하지 않는 것으로 실행 취소가 가능하다.
- ↑ Register(REG or R) : 연산유닛과 연결된 액세스속도가 가장 빠른 기억장치
- ↑ Program Counter(PC) : 다음에 인출할명령어의 주소를 가지고 있는 레지스터, 각 명령어가 인출된 후에는 명령어 길이만큼 주소를 증가시킴으로써 주소를 포인팅함. 분기(jump) 명령어가 실행되는 경우에는 목적지주소로 갱신한다
- ↑ Instruction Register(IR) : 현재 실행중인 명령의 내용을 기억하고 있는 레지스터
- ↑ 각종 산술연산(덧셈, 뺄셈, 곱셈, 나눗셈)과 논리연산(AND, OR, NOT, XOR 등)들을 수행하는 회로들로 이루어진 회로
- ↑ Control Unit : 명령어를 해석하고, 그것을 실행하기 위한 제어 신호들(control signals)을 순차적으로 발생하는 회로
- ↑ 많은 저전력 CPU가 이러한 구조이다. 가령 예전의 Atom CPU가 그러하다.
- ↑ 가령 movl %eax,16(%esp) 같은 더러운 명령어는 레지스터 읽기, 쓰기, 메모리 접근의 세 가지 동작으로 쪼개질 것이다.
- ↑ True dependency인 read-after-write와 다르게, 실행 순서를 섞는 바람에 생긴 원래는 발생할리 없던 문제들이다. 구체적으로 write-after-read나 write-after-write가 있으며, 가령 전자는 앞선 명령어가 읽을 값을 뒤따르는 명령어가 쓰기 동작으로 덮어쓰는 상황을 일컫는다.
- ↑ ReOrder Buffer. 명령어들의 프로그램에서의 원래 순서를 기억하는 장소.
- ↑ Issue Queue. 명령어가 실행될 수 있을 때까지 기다리는 장소.
- ↑ Load-Store Queue. 메모리 명령들이 처리되는동안 들어가있는 장소. 꽤나 복잡하기 때문에 자세한 설명은 아래 Speculative memory disambiguation에서 설명.
- ↑ 다음 사이클에 내가 필요한 값이 준비되며, 실행을 위한 장치를 쓸 수 있을 때
- ↑ branch prediction등의 틀릴 수 있는 동작은 틀렸을 때 취소할 수 있어야 한다.
- ↑ 접근할 메모리 주소가 나올 때까지 대기하고, 또 주소들을 비교하는 일련의 동작들을 위해 LSQ가 필요한 것이다. LSQ는 이게 어떤 것이라고 정의를 설명하는 것보다, 용례로 설명하는 것이 편하다.