나무모에 미러 (일반/어두운 화면)
최근 수정 시각 : 2024-02-14 02:14:31

CPU/구조와 원리

파일:상위 문서 아이콘.svg   상위 문서: CPU
파일:26ee8c2.gif



1. 개요2. 용어3. Inorder CPU
3.1. 물리적인 구조
3.1.1. 시스템 버스3.1.2. CPU 내부 레지스터3.1.3. 마이크로연산과 사이클
3.2. 소프트웨어 구조
3.2.1. 명령어 형식
4. 명령 주기5. 설명
5.1. CPU 마이크로아키텍처5.2. 코어5.3. CPU 아키텍처

1. 개요

CPU의 구조와 원리를 설명한 문서. 거의 모든 종류의 CPU가 하는 일은 요약해보면 대부분 4개 기능이 주다.가장 어려운 부분을 스킵해서 쉬워보이는 건 기분 탓

이 문서는 명령어의 실행 순서를 섞을 수 없는 기초적인 Inorder CPU와 현대의 Out-of-order CPU를 구분하여 서술되었다.

반도체 칩에 대해 이야기 할때 관련 학과로는 보통 전기전자공학과를 많이 떠올리지만, 프로세서 아키텍처[1] 연구개발은 컴퓨터 과학자들이 주도한다.

2. 용어

3. Inorder CPU

3.1. 물리적인 구조

3.1.1. 시스템 버스

시스템 버스란 CPU와 시스템 내의 다른 요소들 사이에 정보를 교환하는 통로로, 다음과 같이 나눌 수 있다. [2]
CPU가 데이터를 기억장치의 특정 장소에 저장하거나 읽어오는 동작을 액세스라고 하는데, 액세스가 시작된 이후 완전히 종결될 때까지 걸리는 시간을 기억장치 쓰기 시간이라고 한다.

3.1.2. CPU 내부 레지스터

CPU를 통해 계산을 하기 위해 필수 불가결한 요소들은 다음과 같다. [3]
데이터 및 인출 사이클에서 CPU 클럭에 따라 각 주기 동안 수행되는 기본적인 동작을 마이크로-연산이라고 한다.

3.1.3. 마이크로연산과 사이클

파일:attachment/CPU/basic-instruction-processing-cycle.jpg
파일:attachment/CPU/basic-instruction-processing-cycle-with-exseption-hangling.jpg
간단화된 CPU 명령어 사이클 예외처리를 추가한 간단한 명령어 사이클

명령어 사이클은 CPU가 기억장치로부터 명령어를 읽어오는 명령어 인출, 실행하는 명령어 실행, 그리고 해당 단계를 부 사이클로 구분하는 인출 사이클실행 사이클이 있다. 인출 사이클은 기억장치의 지정된 위치로부터 명령어를 읽는 과정을 의미하며, 클록의 주기에 대해 마이크로연산을 구할 수 있다.
t0 MAR ← IR
t1 MBR ← M[MAR]
t2 AC ← MBR

3.2. 소프트웨어 구조

3.2.1. 명령어 형식

일반적으로 메인 메모리로부터 메모리 레지스터를 거쳐 프로그램 카운터, 기억장치들에 도달하며, 명령어의 비트 수와 용도 및 구성 방식을 지정하는 형식인 "명령어 형식"으로 지정된다. 기계 명령어의 형식은 일반적으로 연산 코드-피연산자로 구성되어 있다. CPU가 한번에 처리할 수 있는 워드 형식(8비트의 배수) 형식을 통해 이를 정의하며, 지정가능한 워드 수가 많을수록 연산의 종류가 다양화된다.

4. 명령 주기

파일:attachment/CPU/basic-instruction-processing-cycle.jpg
간단화 한 CPU Instruction Cycle(명령 주기)
파일:attachment/CPU/basic-instruction-processing-cycle-with-exseption-hangling.jpg
예외처리를 추가한 간단한 CPU Instruction Cycle(명령주기)

기본 구성으로는 레지스터, 프로그램 카운터[4], 명령어 레지스터[5], ALU, 제어부[6]와 내부 버스 등이 있다. 그 외에도 캐시 메모리 같은 부가 장치도 들어가 있는 경우가 대다수.

5. 설명

명령어의 실행 순서를 섞을 수 있는 현대의 Out-of-order CPU 들은 아래와 같은 순서를 거쳐 명령어를 실행한다.
* Fetch: 실행할 명령어들을 가져온다. 이후 단계들도 그렇지만, 한 번에 보통 4개 정도를 처리하는데, 슈퍼스칼라라고 불리는 기술은 이렇게 한 사이클에 여러 명령어를 처리하는 것을 말한다.
* 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 coherence protocol의 도입이 전체 구조의 차이를 만든다.

5.1. CPU 마이크로아키텍처

  1. 명령어 (instruction)
    CPU의 동작은 할 일을 적어놓은 명령어가 수행되면서 동작한다. 3개의 명령어를 순차적으로 예를 들어보면, '(1) x라는 값을 읽고, (2) x에 1을 더하고, (3) x를 y에 저장해라' 같은 것들이다.
  2. 파이프라인(Pipeline)
    파이프라인은 CPU가 하나의 명령어를 처리하는 과정도 너무 복잡하고 많기 때문에, 이를 잘게 쪼개서 여러 가지 작은 단계로 나누어 처리하는 방식이다. 파이프라인 단계는 여러 가지 나누는 방법이 있지만 현대의 Out-of-Order CPU는 상술한 8단계를 기반으로 나누고 있다. 주목할 점은 완료를 하기 전에는 아직 사용자에게 명령어 수행의 결과가 노출되지 않았다는 것이다. 모종의 이유로 어떤 명령어 수행을 취소해야 하면, 가장 오래된 잘못 수행된 명령어부터 시작해서 이후 모든 명령어를 파이프라인에서 지워버리게 된다.!! 결과론적으로 버릴 명령어 수행에 에너지를 허비했기 때문에 이런 상황은 애초에 피하는 게 좋지만, 그럼에도 피할 수 없는 이유는 아래 추측 실행(Speculative execution)에 설명한다.
  3. Cache (캐시)
    주메모리에서 값을 읽는 동작은 CPU의 명령어 처리 속도에 비하면 한참 느리다. 따라서 이 갭을 줄이기 위해 매우 빠르지만 작은 저장 공간이 CPU에 있는데, 이를 캐시라고 한다.[15] 캐시는 공간이 작기 때문에 새로운 값을 읽어들이려면 기존 값을 다시 원래 위치로 돌려보내야 하는데, 주로 가장 오래된 값을 돌려보내는 방식을 취한다. 이는 최근에 접근한 값을 또 접근할 가능성이 높은 프로그램의 성질을 이용한 것이다.(참조지역성. Principle of locality) 또 한 값을 읽으면 그 근처의 값을 읽을 확률이 높은 점을 활용하기 위해, 어떤 값을 캐시로 가져올 때 주변의 값도 같이 가져온다. (역시 참조지역성의 원리) 이렇게 캐시에 값이 들어오는 덩어리를 캐시 라인이라고 하며, 보통 64바이트의 크기를 가진다. 구체적으로 메모리 주소를 64로 나눴을 때 몫이 같은 데이터들은 한 캐시 라인을 구성하게 된다.
  4. 분기 예측(Branch prediction)
    분기(Branch) 명령어는 어떤 조건이 맞으면 다음에 실행할 명령어의 위치를 임의로 지정할 수 있게 해준다. 이는 같은 명령어들을 반복해서 실행하거나 조건에 따라 다른 일을 하고 싶을 때 사용하는 매우 기본적인 명령어다. 다만 분기(Branch) 명령어는 파이프라인에서 한가지 성능상 문제를 일으키는데, (1) 분기(Branch)가 가리키는 주소로 이동할지 말지', 그리고 (2) 이동하는 주소가 어디인지 알아내는 것은 해독(Decode)이나 실행(Execute) 단계에 와서야 가능하다는 것이다. 다음 명령어 위치를 마냥 기다리자니 파이프라인의 호출(Fetch) 단계가 잠시 놀게 된다. 이를 막고자 다음 명령어 위치를 예측하는 기법을 사용하는데 이것이 분기 예측(Branch prediction) 이다. 당연히 가리키는 주소로 갈지 말지의 방향 예측과, 가리키는 주소 자체의 예측 두 가지가 함께 이루어진다.
  5. 비순차적 명령어 처리(Out-of-Order Execution, OoOE)
    비순차적 명령어 처리(OoOE)는 파이프라인의 송출(Issue) → 실행(Execute) → 회신(Writeback) 단계에 한해서 늦게 온 명령어가 일찍 온 명령어를 새치기할 수 있는 기술이다. 앞에 온 명령어의 처리가 수행될 수 없지만 뒤에 온 명령어는 수행 가능한 경우가 나올 수 있는데, 그러면 CPU를 놀게 하기보다 뒤에 나온 명령어를 먼저 처리하자는 거다. 물론 아무 때나 막 할 수는 없고 이렇게 순서를 바꿔도 사용자가 보는 값이 비순차적 명령어 처리(OoOE)를 하지 않았을 때와 같을 경우만 할 수 있다. 이러한 까다로운 조건 체크 때문에 비순차적 명령어 처리(OoOE)를 달면 속도야 많이 빨라지지만 칩이 커지고 전기도 훨씬 더 먹게 된다. 그래서 초창기엔 휴대기기용 CPU엔 비순차적 명령어 처리(OoOE)가 없었다가 최근에야 최적화를 거치고 사용되기 시작했다.
  6. 추측 실행(Speculative execution)
    파이프라인 단계에서 이미 설명했듯이, 완료(Commit) 단계 전에는 아직 명령어 수행을 취소할 수 있다. 그러므로, 어떤 명령어가 특정 파이프라인 단계에 필요한 정보가 없어서 진행이 막혔을 때, 필요한 정보를 예측해서 높은 확률로 맞힌다면 틀렸을 때의 다소 큰 손해를 넘어서는 이익을 취할 수 있다. 고성능의 CPU는 이러한 예측에 기반한 갖가지 기술들을 적극 활용하고 있다.
    가령 명령어들을 반복 실행하는 소위 반복문이라는 게 있다고 하자. 이 반복문의 맨 마지막 명령어는 종결 조건이 맞지 않는 동안은 반복문의 맨 처음 명령어로 점프하도록 만들어져 있는데, 결과적으로 마지막 반복 말고는 항상 맨 처음 명령어로 점프하기에 높은 확률로 점프를 한다는 예측이 맞게 된다. 이렇게 예측을 하면 굳이 마지막 명령어를 실행(Execute)할 필요도 없이 다음 명령어를 호출(Fetch)할 수 있고, 기다리는 시간을 줄여서 성능을 대폭 상승시킨다. 다만 맨 마지막 반복에서조차도 마지막 명령어는 반복을 더 하려고 예측이 틀리게 된다. 예측이 틀린지 아는 시점은 명령어의 해독(Decode) 혹은 실행(Execute) 단계 수행 직후이며, 틀렸다면 틀린 명령어의 완료(Commit) 단계 때 자기 자신과 이후 호출(Fetch)된 모든 명령어를 파이프라인에서 지우게 된다.

5.2. 코어

5.3. CPU 아키텍처

  1. 커널 / 유저 프로세스: 컴퓨터를 켰을 때 돌아가는 운영체제를 또 다른 말로 커널이라고 하며, 커널 이외에 돌아가는 프로그램 하나 하나는 유저 프로세스라 한다.
  2. 보호 링(protection ring): 운영체제는 컴퓨터의 모든 값을 읽고 쓰고 할 수 있다. 그런데 운영체제에서 돌아가는 유저 프로세스들도 마찬가지면 무슨 일이 벌어질까? 유저 프로세스에 버그가 있거나 악성 코드가 있을 경우 컴퓨터 내의 아무 값이나 읽고 쓸 수 있는 심각한 문제가 발생한다. 따라서 운영체제는 CPU에서 제공된 보호 링이라는 것을 써서 (1) 커널 수준과 (2) 유저 수준을 나누게 된다.
    결과적으로 CPU가 유저 프로세스를 실행하는 동안은 유저 수준에 있게 되며, 유저 프로세스가 커널에 적법한 방법을 통해(시스템 콜) 도움을 요청(가령 파일 읽고 쓰기는 커널의 도움으로 이뤄진다)하고, CPU는 커널 수준으로 변환하고 커널 코드를 실행하게 된다.
  3. 특권 명령(Privileged Instruction): 보호 링과 관련된 개념으로 유저 프로세스가 CPU의 모든 명령어를 사용할 수 있는 것이 아니다. 예를 들어 x86 아키텍처의 HLT는 CPU의 동작을 정지시키는 명령어로 만약 유저 프로세스가 이를 사용할 경우 시스템 전체가 꺼져버릴수도 있다. 그래서 HLT 명령어는 커널 수준에서만 실행할 수 있으며 커널 수준에서만 실행할 수 있는 명령어를 특권 명령이라고 부른다. 만약 유저 수준에서 특권 명령을 실행할 경우 CPU는 실제로 실행시키지 않고 예외를 발생시킨다.
  4. 가상메모리(virtual memory) / 페이지 테이블(page table): 모든 유저 프로세스들이 주메모리의 공간들을 직접 할당 받는다면 어떤 일이 벌어질까? 컴퓨터 환경에 따라서 유저 프로세스가 가정해야 하는 메모리의 주소와 사용 가능한 용량이 자꾸 바뀌게 될 것이다. 프로그래머 입장에서 이러한 현상은 큰 골칫덩어리이며, 따라서 유저 프로세스 각각은 처음 시작할 때 텅 비어있는 똑같은 크기의 가상 메모리를 받게 된다.
    가상 메모리의 접근은 결국에는 실제 메모리의 접근으로 이어지는데, 각 유저 프로세스의 가상 메모리 주소로부터 실제 메모리 주소로의 매핑을 저장한 데이터 구조를 페이지 테이블이라고 하며, 각 유저 프로세스마다 독립된 페이지 테이블이 존재하게 된다.
  5. 콘텍스트 스위칭(context switching): 컴퓨터에는 수많은 프로세스들이 동시에 돌고 있다. 그러나 CPU는 논리적으로 한 코어(굳이 논리라고 하는 것은 SMT 기술 때문)에 한 프로세스만 돌리고 있을 수 있다. 그래서 CPU는 한 프로세스를 어느 정도 돌리다가 다른 프로세스로 전환해서 돌리려고 하는데, 이를 콘텍스트 스위칭이라고 한다. 유저 프로세스가 바뀌면 가상 메모리 공간도 바뀌어야 하므로, 페이지 테이블도 유저 프로세스에 맞게 같이 바뀌게 된다. 사실 커널도 하나의 프로세스와 비슷한 거라서 유저 수준에서 커널 수준으로 갈 때도 콘텍스트 스위칭이 일어나게 된다.
  6. 페이지 테이블 항목(page table entry)의 보호 비트(protection bit): 최신의 커널에는 재미있는 최적화가 있는데, 유저 프로세스의 가상 메모리의 특정 주소는 커널 데이터를 담고 있다는 것이다. 이러면 유저 프로세스가 syscall을 호출해서 커널의 도움을 받으러 갈 때 페이지 테이블을 커널 것으로 교체할 필요가 없다. 그래서 파일을 읽고 쓰거나 네트워크 송수신이 더 빠르게 처리될 수 있다. 그런데 이렇게 되면 유저 프로세스가 커널 데이터를 마구 읽을 수 있지 않을까? 이를 위해 페이지 테이블은 실제 메모리 주소와 함께 보호 비트란 것도 달고 있다. 보호 비트는 여러 가지가 있지만 여기서 중요한 것은 이 실제 메모리 주소는 커널 수준만 읽을 수 있다같은 표식이다. CPU에서 유저 수준에서 유저 프로세스를 돌리다가 메모리 값을 읽는 명령어를 처리하려는데, 이 표식을 발견하면 해당 명령어는 규칙을 위반한 것이므로 실제로는 실행되지 않는다. 리눅스에서 Meltdown 취약점을 막는 KASI(kernel address space isolation) 패치는 이 최적화를 하지 않도록 하는 방식이었고, 그 결과 상당한 성능 저하를 가져오게 되었었다.


[1] MIPS, RISC-V, ARM(CPU)등.[2] 맨 위 사진의 Addr Bus, Data Bus, Ctrl. Bus 부분.[3] 사진의 메인메모리에서 명령어를 받아와, 10비트의 형식으로 전개한다.[4] Program Counter(PC) : 다음에 인출할 명령어의 주소를 가지고 있는 레지스터, 각 명령어가 인출된 후에는 명령어 길이만큼 주소를 증가시킴으로써 주소를 포인팅함. 분기(jump) 명령어가 실행되는 경우에는 목적지 주소로 갱신한다[5] Instruction Register(IR) : 현재 실행 중인 명령의 내용을 기억하고 있는 레지스터[6] Control Unit : 명령어를 해석하고, 그것을 실행하기 위한 제어 신호들(control signals)을 순차적으로 발생하는 회로[7] 가령 movl %eax,16(%esp) 같은 더러운 명령어는 레지스터 읽기, 쓰기, 메모리 접근의 세 가지 동작으로 쪼개질 것이다.[8] True dependency인 read-after-write와 다르게, 실행 순서를 섞는 바람에 생긴 원래는 발생할리 없던 문제들이다. 구체적으로 write-after-read나 write-after-write가 있으며, 가령 전자는 앞선 명령어가 읽을 값을 뒤따르는 명령어가 쓰기 동작으로 덮어쓰는 상황을 일컫는다.[9] ReOrder Buffer. 명령어들의 프로그램에서의 원래 순서를 기억하는 장소.[10] Issue Queue. 명령어가 실행될 수 있을 때까지 기다리는 장소.[11] Load-Store Queue. 메모리 명령들이 처리되는동안 들어가있는 장소. 꽤나 복잡하기 때문에 자세한 설명은 아래 Speculative memory disambiguation에서 설명.[12] 다음 사이클에 내가 필요한 값이 준비되며, 실행을 위한 장치를 쓸 수 있을 때[13] branch prediction등의 틀릴 수 있는 동작은 틀렸을 때 취소할 수 있어야 한다.[14] 접근할 메모리 주소가 나올 때까지 대기하고, 또 주소들을 비교하는 일련의 동작들을 위해 LSQ가 필요한 것이다. LSQ는 이게 어떤 것이라고 정의를 설명하는 것보다, 용례로 설명하는 것이 편하다.[15] 컴퓨터의 메모리는 레지스터, 캐시, 메인 메모리, 외부 디스크 등의 식으로 계층적 구조를 이루는데, 캐시 자체도 여러 개의 레벨(Level)들로 구성되어 L1, L2 캐시 등으로 나뉘기도 한다. 특정 프로세서마다 다르기는 하지만 L1, L2 캐시는 보통 CPU 내부(on-chip)에 위치하고 L3는 off-chip인 경우가 많다. 아무래도 캐시와 CPU 사이의 물리적 거리가 근접할수록 속도는 빨라질 것이기에 현재는 캐시도 CPU 내부에 배치하는 것이 추세라고 할 수 있다.