* '혼자서 공부하는' 컴퓨터구조+운영체제 이지만, 혼자보단 여럿이하면 더 좋으니 혼공 학습단 13기와 함께합니다 :)
[2주차 학습 목록] (*ˊᵕˋ*)ノ
Ch04. CPU의 작동 원리
01. ALU와 제어장치
02. 레지스터
03. 명령어 사이클과 인터럽트
Ch05. CPU 성능 향상 기법
01. 빠른 CPU를 위한 설계 기법
02. 명령어 병렬 처리 기법
03. CISC와 RISC
Ch04. CPU의 작동 원리
4-1. ALU와 제어장치
앞주에서 배웠듯이 ALU는 계산하는 부품이다. 1+2 계산을 할 때 1과 2라는 피연산자와 + 라는 연산이 필요하듯, ALU도 계산을 하기 위해선 피연산자와 연산이 필요하다. 따라서 피연산자에 대한 부분인 '피연산자'를 레지스터로부터 받고, 연산에 대한 부분인 '제어신호'를 제어장치로부터 받는다.
--> ALU는 레지스터로부터 피연산자를, 제어장치로부터 제어 신호를 받아들인다.
ALU가 연산을 수행하고 나오는 결과는 특정 숫자, 문자 혹은 메모리 주소가 될 수 있다. 이때 이 결과값은 바로 메모리에 저장되지 않고 일시적으로 레지스터에 저장이 된다. 그 이유는, CPU가 메모리에 접근하는 속도보다 레지스터에 접근하는 속도가 더 빠르기 때문이다! (CPU가 메모리에 접근을 많이하면 할 수록 프로그램 실행 속도가 늦어진다.) 이렇게 결과값 말고도 그 결과에 대한 부가 혹은 추가적인 정보를 함께 내보내야 할 때도 있을 것이다. 그러한 연산 결과에 추가적인 상태 정보를 '플래그(flag)'라고 한다. 따라서 ALU는 결과 값을 레지스터로 내보내고, 플래그는 플래그 값들이 따로 저장되는 플래그 레지스터로 내보낸다.
--> 플래그(flag)란, 연산 결과에 대한 추가적인 상태 정보를 의미한다.
--> ALU는 결과 값을 레지스터로, 플래그는 플래그 레지스터로 내보낸다.
[PLUS]
- 오버플로우(overflow) : 연산 결과가 연산 결과를 담을 레지스터보다 큰 상황
- 대표적인 플래그 종류 및 의미
플래그 종류 | 의미 | (해당 플래그가) 1인 경우 | (해당 플래그가) 0인 경우 |
부호 플래그 | 연산 결과의 부호 | 계산 결과가 음수임을 의미 | 계산 결과가 양수임을 의미 |
제로 플래그 | 연산 결과가 0인지의 여부 | 연산 결과가 0임을 의미 | 연산 결과가 0이 아님을 의미 |
캐리 플래그 | 연산 결과 올림수 혹은 빌림수 발생 여부 | 올림수나 빌림수가 발생함을 의미 | 올림수나 빌림수가 발생하지 않음을 의미 |
오버플로우 플래그 | 오버플로우 발생 여부 | 오버플로우가 발생함을 의미 | 오버플로우가 발생하지 않음을 의미 |
인터럽트 플래그 | 인터럽트 가능 여부 | 인터럽트가 가능함을 의미 | 인터럽트가 불가능함을 의미 |
슈퍼바이저 플래그 | 실행 모드 의미 (커널 모드 or 사용자 모드) |
커널 모드로 실행 중임을 의미 | 사용자 모드로 실행 중임을 의미 |
제어장치는 제어 신호를 내보내고, 명령어를 해석하는 부품이다. 그리고 제어 신호는 컴퓨터 부품들을 관리하고 작동시키기 위한 일종의 전기 신호이다. 제어 장치가 무엇을 받아들이고, 무엇을 내보내는지를 중점으로 이해해보자.
첫째, 제어 장치는 클럭 신호를 받아들인다.
클럭(clock)이란, 컴퓨터의모든 부품을 일사분란하게 움직일 수 있게 하는 시간 단위이다. 클럭의 주기에 맞춰 레지스터의 이동이 이루어지거나, ALU에서 연산이 수행되거나, CPU가 메모리에 저장된 명령어를 읽어 들인다. 여기서 오해하지 말아야 할 점은, 컴퓨터 부품들은 클럭이라는 주기에 맞춰 작동할 뿐이지 주기마다 작동하는 것은 아니다!!
둘째, 제어 장치는 '해석해야 할 명령어'를 받아들인다.
CPU가 해석해야 할 명령어는 '명령어 레지스터'라는 곳에 저장되는데 제어 장치는 이 명령어 레지스터로부터 해석할 명령어를 받아들이고 해석한 뒤 제어 신호를 발생시켜 컴퓨터 부품들에게 수행해야 할 내용들을 알려준다.
셋째, 제어 장치는 플래그 레지스터 속 플래그 값을 받아들인다.
제어 장치는 플래그 값을 받아들이고 이를 참고하여 제어 신호를 발생시킨다.
넷째, 제어 장치는 시스템 버스, 그 중에서 제어 버스로 전달된 제어 신호를 받아들인다.
제어 신호는 CPU뿐만 아니라 입출력 장치를 비롯한 CPU 외부 장치도 발생시킬 수 있다.
--> 제어 장치는 클럭, 현재 수행할 명령어, 플래그, 제어 신호를 받아들인다.
지금까지 제어 장치가 받아들이는 정보에 대해 알았다면 이제는 제어 장치가 내보내는 정보를 알아보자.
내보내는 정보에는 크게 CPU 외부/내부에 전달하는 제어 신호로 나뉜다. 제어 장치가 CPU 외부에 제어 신호를 전달한다는 것은 곧, 제어 버스로 내보낸다는 말과 같다. 외부로 전달하는 제어 신호에는 메모리에 전달하는 신호와 입출력장치(보조기억장치 포함)에 전달하는 신호로 나뉜다. 제어 장치가 값을 읽기/쓰기를 하고 싶은 위치가 어디냐에 따라 제어 신호를 보낸다. 메모리에 있다면 메모리에 제어 신호를 내보내고, 입출력장치에 있는 값을 읽기/쓰기하고 싶을 때는 입출력장치로 제어 신호를 내보낸다.
외부와 반대로 CPU 내부에 전달하는 제어 신호에는 크게 ALU와 레지스터에 전달하는 제어신호로 나뉜다. ALU에는 수행할 연산을 지시하기 위해서, 레지스터에는 레지스터 간 데이터 이동을 시키거나 저장된 명령어를 해석하기 위해 제어 신호를 내보낸다.
--> 제어 장치는 CPU 내부와 외부로 제어 신호를 내보낸다.
4-2. 레지스터
상용화된 여러 CPU 속 레지스터들은 CPU마다 이름, 크기, 종류가 매우 다양하다. 이러한 것들은 각 CPU 제조사 홈페이지나 공식 문서 등을 살펴보면 확인할 수가 있다. 이 책에서는 그 모든 레지스터를 다 다룰 수 없기에 중요하게 다루는 레지스터, 많은 CPU가 공통으로 포함하고 있는 여덟 개의 레지스터를 학습해 볼 것이다.
* 상용화된 CPU 중 가장 대중적인 두가지는 Intel과 ARM이다.
레지스터는 각자마다의 역할이 있고, 그에 맞는 내용을 저장한다. 한번에 다 외우려고 하다보면 부담이 될 수 있으니, 차근차근 여러번 읽다보면 자연스레 습득이 될 것 이다. 자, 이제 한번 여덟가지의 레지스터를 알아보자.
첫번째, 프로그램 카운터
프로그램 카운터(PC: Program Counter)는 메모리에서 읽어 들일 명령어 주소를 저장한다.
* 그래서 '명령어 포인터(IP: Instruction Pointer)'라고 부르는 CPU도 있다.
두번째, 명령어 레지스터
명령어 레지스터(IR: Instruction Register)는 메모리에서 읽어들인 명령어를 저장한다.
세번째, 메모리 주소 레지스터
메모리 주소 레지스터(MAR: Memory Address Register)는 메모리의 주소를 저장한다.
네번째, 메모리 버퍼 레지스터
메모리 버퍼 레지스터(MBR: Memory Buffer Register)는 메모리와 주고받을 값(=데이터와 명령어)을 저장한다.
* 그래서 '메모리 데이터 레지스터(MDR: Memory Data Register)'라고도 부른다.
설명만으로는 어떻게 작동이 되는지 이해하기 어려우니 그림과 함께 한번 알아보자.
1. 예시로, CPU로 실행할 프로그램이 1000번지부터 1500번지까지 저장되어 있다고 가정하자. 그리고 1000번지에는 1101(2)이 저장되어 있다고 하자.
* 여기서 명령어들은 각각 하나의 메모리 번지를 차지하고 있다고 가정하자.
2. 프로그램을 처음부터 실행하기 위해서 프로그램 카운터에는 1000이 저장된다. 즉, 메모리에서 가져올 명령어의 주소인 1000번지가 담긴 것이다.
3. 1000번지를 읽어 들이기 위해서는 주소 버스로 1000번지를 내보내야 한다. 이를 위해서는 메모리 주소 레지스터에 1000이 저장된다.
4. '메모리 읽기' 제어 신호는 제어 버스를 통해서, 메모리 주소 레지스터 값은 주소 버스를 통해서 메모리로 보내진다.
5. 메모리 1000번지에 저장된 값은 데이터 버스를 통해서 메모리 버퍼 레지스터로 전달되고, 프로그램 카운터는 +1 증가되어 다음 명령어를 읽을 준비를 한다.
* 프로그램 카운터는 지속적으로 증가하며 계속해서 다음 명령어를 읽어 들일 준비를 한다.
이 과정이 반복되면서 CPU는 프로그램을 차례로 실행해 나간다.
6. 메모리 버퍼 레지스터에 저장된 값은 명령어 레지스터로 이동한다.
7. 제어 장치는 명령어 레지스터의 명령어를 해석하고 제어 신호를 발생시킨다.
다섯째, 범용 레지스터
범용 레지스터(general purpose register)는 데이터와 주소를 모두 저장할 수 있다.
여섯째, 플래그 레지스터
플래그 레지스터(flag register)는 연산 결과 또는 CPU 상태에 대한 부가적인 정보를 저장한다.
일곱번째, 스택 포인터
스택 포인터(stack pointer)는 스택 주소 지정 방식이라는 지정 방식에 사용되는데, 스택에 마지막으로 저장한 값의 위치를 저장하는 레지스터를 말한다. 여기서 스택 주소 지정 방식이란, 스택과 스택 포인터를 이용한 주소 지정 방식이다.
--> 쉽게 말하자면, 스택 포인터는 스택이 어디까지 데이터가 채워져 있는가에 대한 표시라고 생각하면 된다. 이러한 스택이라는 것은 메모리 안에 스택처럼 사용할 영역인 '스택 영역'에 있다.
여덟번째, 베이스 레지스터
베이스 레지스터(base-register)는 변위 주소 지정 방식이라는 주소 지정 방식에 사용된다. 변위 주소 지정 방식(displacement addressing mode)이란 오퍼랜드 필드 값(=변위)과 특정 레지스터의 값을 더하여 유효 주소를 얻어내는 주소 지정 방식을 말한다. 따라서 변위 주소 지정 방식을 사용하는 명령어는 "연산코드(이런 내용을 수행하라) - 레지스터(이 레지스터 값과) - 오퍼랜드(이 주소를 더한 곳에 있는 데이터로)" 세가지의 필드로 이루어져 있다. 이때, 변위 주소 지정 방식은 상대 주소 지정 방식, 베이스 레지스터 주소 지정 방식 등으로 나뉜다. 여러 방식으로 나뉘지만 대표적인 상대 주소 지정 방식과 베이스 레지스터 주소 지정 방식만 알아보자.
- 상대 주소 지정 방식(relative addressing mode) : 오퍼랜드와 프로그램 카운터 값을 더하여 유효 주소를 얻는 방식
- 베이스 레지스터 주소 지정 방식(base-register addressing mode) : 오퍼랜드와 베이스 레지스터의 값을 더하여 유효 주소를 얻는 방식
* 여기서 베이스 레지스터는 '기준 주소', 오퍼랜드는 '기준 주소로부처 떨어진 거리'로서의 역할을 한다. 따라서 베이스 레지스터 속 기준 주소로부터 얼마나 떨어져 있는 주소에 접근할 것인지를 연산하여 유효 주소를 얻어내는 방식이 바로 베이스 레지스터 주소 지정 방식이다.
* 앞서 말한 프로그램 카운터와 스택 포인터, 베이스 레지스터는 주소 지정에 사용될 수 있는 특별한 레지스터이다.
4-3. 명령어 사이클과 인터럽트
CPU는 흐름을 반복하며 명령어들을 처리해 나가는데, 이렇게 하나의 명령어를 처리하는 정형화된 흐름을 명령어 사이클이라고 한다. 간혹 이 명령어 사이클이 끊기는 상황이 발생할때가있는데 그러한 상황을 인터럽트라고 한다.
명령어 사이클(instruction cycle)이란, 프로그램 속 각각의 명령어들이 일정한 주기가 반복되며 실행되는 주기를 말한다. 이 명령어 사이클의 첫번째 과정은 명령어를 메모리에서 CPU로 가져오는 단계인 '인출 사이클(fetch cycle)'이다. 그 다음으로 두번째 과정은 CPU로 가져온 명령어를 실행하는 단계인 '실행 사이클(execution cycle)'이다. 프로그램을 이루는 수많은 명령어는 일반적으로 인출과 실행 사이클을 반복하며 실행된다.
But, 모든 명령어가 이렇게 간단히 실행되는 것은 아니다! 앞서 배운 간접 주소 지정 방식 같은 경우에는 명령어를 인출하여 CPU로 가져왔다고 해도 바로 실행 사이클에 돌입할 수 없다. 명령어를 실행하기 위해 메모리 접근을 한 번 더 해야하기 때문이다. 이러한 단계를 '간접 사이클(indirect cycle)'이라고 한다.
여기서 하나 더 고려해야 할 것이 있는데 바로 인터럽트이다!
인터럽트(interrupt)란, CPU의 정상적인 작업을 방해하는 신호를 의미한다. CPU의 작업을 방해하는 것이 필요한가? 생각할 수 있다. CPU가 작업을 잠시 중단해야 할 정도인 'CPU가 꼭 주목해야 할 때' 혹은 'CPU가 얼른 처리해야 할 다른 작업이 생겼을 때' 발생할 것이다. 인터럽트가 어떤 상황에서 발생하는지 종류를 통해 알아보자.
* 참고로 인터럽트의 종류를 구분하는 통일된 기준은 없다고 한다! 인텔의 공식 문서를 참조하여 알아보자.
인터럽트의 종류에는 크게 동기 인터럽트와 비동기 인터럽트가 있다.
- 동기 인터럽트(synchronous interrupts) : CPU에 의해 발생하는 인터럽트이다.
CPU가 실행하는 프로그래밍상의 오류와 같은 예외적인 상황에 마주쳤을 때, 발생하는 인터럽트
--> 그래서 '예외(exception)'라고 불린다.
- 비동기 인터럽트(asynchronous interrupts) : 주로 입출력장치에 의해 발생하는 인터럽트이다.
입출력장치에 의한 비동기 인터럽트는 알림 역할을 한다. 일반적으로 비동기 인터럽트를 인터럽트라 칭하기도 한다.
비동기 인터럽트는 어떻게 처리가 되는지 알아보자.
앞서 여러 다른 종류의 인터럽트를 설명했지만, CPU가 인터럽트를 처리하는 방식은 종류를 막론하고 모두 다 같다. 다음과 같은 순서로 처리가 된다.
1. 입출력장치는 CPU에 '인터럽트 요청 신호'를 보낸다.
2. CPU는 실행 사이클이 끝나고 명령어를 인출하기 전 항상 인터럽트 여부를 확인한다.
3. CPU는 인터럽트 요청을 확인하고 '인터럽트 플래그'를 통해 현재 인터럽트를 받아들일 수 있는지 여부를 확인한다.
4. 인터럽트를 받아들일 수 있다면 CPU는 지금까지의 작업을 백업한다.
5. CPU는 '인터럽트 벡터'를 참조하여 '인터럽트 서비스 루틴'을 실행한다.
6. 인터럽트 서비스 루틴 실행이 끝나면 4번에서 백업해 둔 작업을 복구하여 실행을 재개한다.
여기서, 알아야 할 키워드들을 알아보자.
인터럽트 요청 신호란, CPU의 정상적인 실행 흐름을 끊을 때, 인터럽트하기 전 CPU에게 물어보는 신호를 의미한다. 이때 CPU가 인터럽트 요청을 수행하기 위해선 플래그 레지스터의 인터럽트 플래그가 활성화 되어 있어야 한다. 인터럽트 플래그는 비동기 인터럽트를 받아들일지, 무시할지를 결정하는 플래그이다. 하지만, 모든 비동기 인터럽트를 인터럽트 플래그로 막을 수 있는 것은 아니다. 정전이나 하드웨어 고장으로 인한 인터럽트가 막을 수 없는 인터럽트에 해당한다. 이런 인터럽트는 가장 먼저 처리해야 하는 인터럽트다. 따라서 비동기 인터럽트에는 인터럽트 플래그로 막을 수 있는 인터럽트(maskable interrupt)와 막을 수 없는 인터럽트(non maskable interrupt)가 있다.
CPU가 인터럽트 요청을 받아들이기로 했다면 인터럽트 서비스 루틴이라는 프로그램을 실행한다.
인터럽트 서비스 루틴(ISR: interrupt Service Routine)은 인터럽트를 처리하기 위한 동작들로 이루어진 프로그램이다. 혹은 인터럽트 핸들러(interrupt handler)라고도 부른다. 인터럽트 서비스 루틴은 어떤 인터럽트가 발생했을 때 해당 인터럽트를 어떻게 처리하고 작동해야 할지에 대한 정보로 이루어진 프로그램이다. 인터럽트를 처리하는 방법은 입출력장치마다 다르므로 각기 다른 인터럽트 서비스 루틴을 가지고 있다. 즉, 메모리에는 여러 개의 인터럽트 서비스 루틴이 저장되어 있다.
이렇게 각기 다른 인터럽트 서비스 루틴이 있으니 CPU는 구분하기 위해 인터럽트 벡터를 사용한다. 인터럽트 벡터(interrupt vector)란, 인터럽트 서비스 루틴을 구분하기 위해 인터럽트 벡터를 이용한다. 인터럽트 벡터를 알면 인터럽트 서비스 루틴의 시작 주소를 알 수 있기 때문에 특정 인터럽트 서비스 루틴을 처음부터 실행할 수 있다.
이렇게 인터럽트가 발생하면 그 전까지 레지스터에 저장되어 있던 값들은 어떻게 해야할까?
인터럽트 요청을 받기 전까지 CPU가 수행하고 있었던 일은 인터럽트 서비스 루틴이 끝나면 되돌아와서 마저 수행을 마쳐야하기 때문에 작업 내역들은 어딘가에 백업을 해야할 것이다. 이러한 작업 내용들은 스택영역에 백업이 된다. 인터럽트가 처리가 다 끝나면 스택에 저장해 둔 값을 다시 불러와 작업을 재개한다.
* 결국 CPU는 앞서 말한 모든 사이클들을 반복해 나가며 프로그램을 실행한다!
Ch05. CPU 성능 향상 기법
5-1. 빠른 CPU를 위한 설계 기법
앞서 학습한 내용 중 '클럭'이라는 것을 배웠다. 클럭 주기에 맞춰 컴퓨터 부품들이 움직인다고 배웠다. 그러면 클럭 신호가 빠르게 반복되면 CPU를 비롯한 컴퓨터 부품들은 그만큼 더 빠르게 움직일 것이다. 즉, 클럭 속도가 높아지면 CPU는 명령어 사이클을 더 빠르게 반복할 것이고 그 외 다른 부품들도 그에 맞춰 빠르게 작동할 것이다.
그래서 클럭 속도는 CPU 속도 단위로 간주되기도 한다!
클럭 속도는 헤르츠(Hz) 단위로 측정한다. 이는 1초에 클럭이 몇 번 반복되는지를 나타낸다. 클럭이 1초에 1000번 반복되면 CPU 클럭 속도는 1000Hz인 셈이다!
* 1GHz = 1,000,000,000(10^9)Hz
여기서 주의할 점!
'클럭'이라는 단어만 보고 속도가 매번 일정하게 유지된다고 생각할 수 있지만, 실제로는 그렇지 않다. CPU는 계속 일정한 클럭 속도를 유지하기보단 고성능을 요구하는 순간에는 순간적으로 속도를 높이고, 그렇지 않은 경우에는 유연하게 속도를 낮추기도 한다. 최대 클럭 속도를 강제로 더 끌어올릴수도 있는데, 이러한 기법을 '오버클럭킹(overclocking)'이라고 한다.
곰곰히 보면 '그렇다면 클럭 속도만 엄청 높이면 CPU가 엄청 빨라지겠네?' 라고 생각할 수 있다.
하지만 아쉽게도 그렇지 않는다. 많이들 CPU에 무리가 가는 작업을 장시간 하게되면 컴퓨터가 뜨겁게 달아오르는 것을 경험해본 적이 있을 것이다. 이처럼 클럭 속도를 엄청 높이게 되면 이러한 발열 문제가 더욱더 심각해지게 된다.
따라서 클럭 속도를 많이 높이는 것만으로 CPU의 성능을 올리는 것에는 한계가 있다!
그렇다면, 그 외 다른 방법이 무엇이 있을까?
대표적인 방법으로는 CPU의 코어와 스레드 수를 늘리는 방법이다. 방법을 알아보기 전에, 용어부터 알아보자!
우리가 지금까지 CPU의 정의로 알고 있었던 '명령어를 실행하는 부품'은 오늘날의 코어(core)라는 용어로 사용된다. 즉, 코어란 CPU 내에서 명령어를 실행하는 부품이다. 흔히들 '8코어'라는 용어들을 들어봤을텐데, 이것은 명령어를 실행하는 부품을 여덟 개 포함하고 있는 것이라고 이해하면 된다.
--> 코어는 CPU 내 명령어를 실행하는 부품으로 여러 개 있을 수가 있다!
코어를 여러개 포함하고 있는 CPU를 멀티코어(multi-core) CPU 혹은 멀티코어 프로세서라고 부른다. 당연히 멀티코어의 처리 속도는 단일코어보다 더 빠르다. CPU 종류는 CPU 안에 코어가 몇 개 포함되어 있는지에 따라 하단 표와 같이 나뉜다. 이때, 싱글코어를 제외한 나머지는 다 멀티코어에 해당한다.
코어 수 | 프로세서 명칭 |
1 | 싱글코어(single-core) |
2 | 듀얼코어(dual-core) |
3 | 트리플코어(triple-core) |
4 | 쿼드코어(quad-core) |
6 | 헥사코어(hexa-core) |
8 | 옥타코어(octa-core) |
10 | 데카코어(deca-core) |
12 | 도데카코어(dodeca-core) |
그렇다면 코어를 2개, 3개, 100개로 늘리면 연산 처리 속도도 2배, 3배, 100배 더 빨라지는가?
안타깝게도 CPU 연산 속도가 코어 수에 무조건 비례하여 증가하지는 않는다. 처리할 연산을 적절히 분배되지 않거나 처리하고자 하는 작업량보다 코어 수가 지나치게 많다면 CPU 연산 속도가 마냥 비례하지는 않는다.
* 중요한 것은 코어마다 각각 처리할 명령어들을 얼마나 적절하게 분배하느냐에 따라 연산 속도는 달라진다!
다음으로 스레드와 멀티스레드를 알아보자.
스레드(thread)의 사전적 의미는, '실행 흐름의 단위'이다. 여기서 혼동하지 말아야 할 것은 CPU에서 사용되는 스레드와 프로그래밍에서 사용되는 스레드는 용례가 다르다. 이렇게 기억하면 쉽다. 스레드에는 CPU에서 사용되는 하드웨어적 스레드가 있고, 프로그램에서 사용되는 소프트웨어적 스레드가 있다.
스레드를 하드웨어적으로 정의하면 '하나의 코어가 동시에 처리하는 명령어 단위'를 의미한다. 우리가 지금까지 배운 CPU는 1코어 1스레드 CPU였다. 반면, 하나의 코어로 여러 명령어를 동시에 처리하는 CPU를 멀티스레드(multithread)프로세서 또는 멀티스레드 CPU라고 한다. 즉, 여러 개의 하드웨어적 스레드를 지원하는 CPU를 멀티스레드 프로세서라고 한다.
* 참고) 인텔의 멀티스레드 기술을 하이퍼스레딩(hyper-threading)이라고 한다.
반대로 스레드를 소프트웨어적으로 정의하면 '하나의 프로그램에서 독립적으로 실행되는 단위'를 의미한다. 하나의 프로그램은 실행되는 과정에서 한 부분만 실행될 수도 있지만, 프로그램의 여러 부분이 동시에 실행될 수도 있다.
* 이 챕터에서 배울 스레드는 하드웨어적 스레드이다.
혼동 방지를 위해 소프트웨어적 정의된 스레드를 스레드, CPU에서 사용되는 스레드를 하드웨어 스레드라고 하자!
5-2. 명령어 병렬 처리 기법
명령어를 동시에 처리하여 CPU를 한시도 쉬지 않고 작동시키는 기법인 명령어 병렬 처리 기법(ILP: Instruction-Level Parallelism)을 알아보자. 대표적인 명령어 병렬 처리 기법에는 1)명령어 파이프 라이닝, 2)슈퍼스칼라, 3)비순차적 명령어 처리가 있다.
명령어 파이프라인을 이해해기 위해선 하나의 명령어가 처리되는 전체 과정을 비슷한 시간 간격으로 나누어 보아야 한다. 명령어 처리 과정을 클럭 단위로 나눠보면 다음과 같이 나눌 수 있다.
1) 명령어 인출(Instruction Fetch)
2) 명령어 해석(Instruction Decode)
3) 명령어 실행(Wxecute Instruction)
4) 결과 저장(Write Back)
* 유의할 점은, 위 단계만이 정답은 아니다...!
여기서 중요하게 봐야할 점은 같은 단계가 겹치지만 않는다면 CPU는 '각 단계를 동시에 실행할 수 있다'는 점이다.
위 그림처럼 명령어를 겹쳐서 수행하면 명령어 하나하나 실행하는 것보다 훨씬 더 효율적으로 처리할 수 있을 것이다.
이것과 같이 마치 공장 생산 라인처럼 명령어들을 명령어 파이프라인(instruction pipeline)에 넣고 동시에 처리하는 기법을 명령어 파이프라이닝(instruction pipelining)이라고 한다.
--> 명령어 파이프라이닝은 동시에 여러 개의 명령어를 겹쳐 실행하는 기법이다.
순차적으로 명령어를 처리하는 경우와 비교해보면 확실히 겹쳐서 실행하는 명령어 파이프라이닝 기법이 더 효율적으로 보일 것이다.
이처럼 파이프 라이닝이 높은 성능을 가져오긴 하지만, 특정 상황에선 성능 향상에 실패하는 경우도 있다.
이러한 상황을 파이프라인 위험(pipeline hazard)이라고 부른다.
이 파이프라인 위험에는 크게 데이터 위험, 제어 위험, 구조적 위험이 있다.
먼저, 데이터 위험(data hazard)은 명령어 간 '데이터 의존성'에 의해 발생한다. 따라서 모든 명령어를 동시에 처리할 수는 없다. 어떤 명령어는 이전 명령어를 끝까지 실행해야만 비로소 실행할 수 있는 경우가 있다. 예를 들어, 두개의 명령어가 다음과 같다고 하자.
명령어 1 : T1 <-- T2 + T3
명령어 2 : T4 <-- T1 + T5
레지스터 이름을 편의상 T1, T2, T3, T4, T5 라고 하고, '왼쪽 레지스터에 오른쪽 결과를 저장하라'는 <-- 기호로 표기하겠다.
이러한 경우, 명령어 1을 수행해야만 명령어 2를 수행할 수가 있다. 이처럼 데이터 의존적인 두 명령어를 무작정 동시에 실행하려고 하면 파이프라인이 제대로 작동하지 않을 것인데 이러한 것을 '데이터 위험'이라고 한다.
그 다음, 제어 위험(control hazard)은 주로 분기 등으로 인한 '프로그램 카운터의 갑작스러운 변화'에 의해 발생한다. 기본적으로 프로그램 카운터는 '현재 실행 중인 명령어의 다음 주소'로 갱신된다. 하지만 프로그램 실행 흐름이 바뀌어 명령어가 실행되며 프로그램 카운터 값에 갑작스러운 변화가 생긴다면 명령어 파이프라인에 미리 가져 와서 처리하고 있던 명령어들은 더이상 쓸모가 없어지게 된다. 이러한 것을 '제어 위험'이라고 한다.
* 참고) 제어 위험을 위해 사용하는 기술 중 하나가 분기 예측(branch prediction)이다. 분기 예측이란, 프로그램이 어디로 분기할지 미리 예측한 후 그 주소를 인출하는 기술이다.
마지막으로 구조적 위험(structural hazard)은 명령어들을 겹쳐 실행하는 과정에서 서로 다른 명령어가 동시에 ALU, 레지스터 등과 같은 CPU 부분을 사용하려고 할 때 발생한다. 이 구조적 위험은 자원 위험(resource hazard)이라고도 부른다.
파이프라이닝은 단일 파이프라인으로도 구현이 가능하지만, 오늘 날 대부분의 CPU에서는 여러 개의 파이프라인을 이용한다. 이처럼, CPU 내부에 여러 개의 명령어 파이프라인을 포함한 구조를 슈퍼스칼라(superscalar)라고 한다.
명령어 파이프라인을 하나만 두는 것이 마치 하나의 공장 생산 라인을 두는 것과 같다면, 슈퍼스칼라는 공장 생산 라인을 여러 개 두는 것과 같다고 보면 된다.
슈퍼스칼라 구조로 명령어 처리가 가능한 CPU를 슈퍼스칼라 프로세서 또는 슈퍼스칼라 CPU라고 한다.
슈퍼스칼라 프로세서는 이론적으로는 파이프라인 개수와 프로그램 처리
속도가 비례한다. 하지만 파이프라인 위험 등의 예상치 못한 문제가 있어서 실제로는 반드시 파이프라인 개수에 비례하여 속도가 빨라지지는 않는다. 이렇기 때문에 슈퍼스칼라 방식을 사용한 CPU는 파이프라인 위험을 방지하기 위해 고도로 설계되어야 한다.
--> 슈퍼스칼라는 여러 개의 명령어 파이프라인을 두는 기법이다.
마지막 병렬 처리 기법은 비순차적 명령어 처리(OoOE: Out-of-order execution)이다. 보통 줄여서 OoOE로 부른다.
이름에서도 알 수 있듯 명령어들을 순차적으로 실행하지 않는 기법이다. 명령어의 '합법적인 새치기'라고 볼 수 있다.
앞서 말한 명령어 파이프라이닝과 슈퍼스칼라 기법은 모두 여러 명령어의 순차적인 처리를 상정한 기법이다. 프로그램을 위에서 아래로 차례차례 실행하는 방식이었다. 하지만 파이프라인 위험과 같은 예상치 못한 문제들로 인해 이따금씩 명령어는 곧바로 처리되지 못하기도 한다. 그럴때 명령어를 순차적으로만 실행하지 않고 순서를 바꿔 실행해도 무방한 명령어들을 먼저 실행하여 명령어 파이프라인이 멈추는 것을 방지하는 기법을 비순차적 명령어 처리 기법이라고 한다.
--> 비순차적 명령어 처리 기법은 파이프라인의 중단을 방지하기 위해 명령어를 순차적으로 처리하지 않는 명령어 병렬 처리 기법이다.
5-3. CISC와 RISC
'파이프라이닝 하기 쉬운 명령어란 무엇일까?', '명령어가 어떻게 생겨야 파이프라이닝에 유리할까?' 궁금할 수 있다.
이와 관련하여 CPU의 언어인 ISA와 각기 다른 성격의 ISA를 기반으로 설계된 CISC, RISC를 알아보자.
CPU가 이해할 수 있는 명령어들의 모음을 명령어 집합(instruction set) 또는 명령어 집합 구조(ISA: Instruction Set Architecture, 이하 ISA)라고 한다. 즉, CPU마다 ISA가 다를 수 있다는 말이다.
현대 ISA의 양대 산맥인 CISC, RISC에 대해 알아보자.
먼저, CISC는 Complex Instruction Set Computer의 약자이다. 그대로 해석을 하면 복잡한 명령어 집합을 활용하는 컴퓨터를 의미한다. 여기서 컴퓨터를 CPU라고 생각해도 괜찮다. CISC는 다양하고 강력한 기능의 명령어 집합을 활용하기 때문에 명령어의 형태와 크기가 다양한 가변 길이 명령어를 활용한다.
--> CISC는 복잡하고 다양한 수의 가변 길이 명령어 집합을 활용한다.
이 CISC의 한계에 원칙을 추가하여 등장한것이 RISC이다.
RISC는 Reduced Instruction Set Computer의 약자이다. 이름처럼 CISC에 비해 명령어 종류가 적다. 그리고 CISC와 달리 짧고 규격화된 명령어, 되도록 1클럭 내외로 실행되는 명령어를 지향한다. 즉, RISC는 고정 길이 명령어를 활용한다.
--> RISC는 단순하고 적은 수의 고정 길이 명령어 집합을 활용한다.
* RISC는 짧고 규격화된 명령어를 활용하기에 명령어 파이프라이닝에 유리하다.
최종! CPU의 언어인 ISA와 각기 다른 성격의 ISA 기반으로 만들어진 CPU 설계 방식인 CISC와 RISC에 대해 알아보았고,
다음의 표와 같이 차이점을 정리할 수가 있다.
CISC | RISC |
복잡하고 다양한 명령어 | 단순하고 적은 명령어 |
가변 길이 명령어 | 고정 길이 명령어 |
다양한 주소 지정 방식 | 적은 주소 지정 방식 |
프로그램을 이루는 명령어의 수가 적음 | 프로그램을 이루는 명령어의 수가 많음 |
여러 클럭에 걸쳐 명령어 수행 | 1클럭 내외로 명령어 수행 |
파이프라이닝하기 어려움 | 파이프라이닝하기 쉬움 |
[과제]
- 필수과제
01) 125p
02. 설명에 맞는 레지스터를 보기에서 찾아 빈칸을 채워 보세요.
[보기] 프로그램 카운터, 명령어 레지스터, 플래그 레지스터, 범용 레지스터
(1) : 연산 결과 혹은 CPU 상태에 대한 부가 정보를 저장하는 레지스터
(2) : 메모리에서 가져올 명령어의 주소를 저장하는 레지스터
(3) : 데이터와 주소를 모두 저장할 수 있는 레지스터
(4) : 해석할 명령어를 저장하는 레지스터
답)
(1) - 플래그 레지스터
(2) - 프로그램 카운터
(3) - 범용 레지스터
(4) - 명령어 레지스터
02) 155p
04. 다음 그림은 멀티코어 CPU를 간략하게 도식화한 그림입니다. 빈칸에 알맞은 용어를 써 넣으세요.
답)
코어(core)
- 선택과제
Q. 코어와 스레드, 멀티 코어와 멀티 스레드의 개념 정리하기.
코어(core)란, CPU 내에서 명령어를 실행하는 부품을 말한다.
멀티 코어(multi-core)란, 코어가 둘 이상인 것을 말한다. 또한, 멀티 코어 프로세스란 여러 개의 코어를 포함하는 CPU를 말한다.
스레드(thread)란, 하드웨어적으로 정의한다면 하나의 코어가 동시에 처리하는 명령어 단위를 말하고, 소프트웨어적으로 정의한다면 하나의 프로그램에서 독립적으로 실행되는 단위를 말한다.
멀티 스레드(multi-thread)란, 하나의 코어로 여러 개의 명령어를 동시에 실행하는 단위이다. 또한, 멀티 스레드 프로세스란 하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU를 말한다.
'Studying > 혼공학습단' 카테고리의 다른 글
[혼공컴운]6주차_Ch.14 - 15 (0) | 2025.02.23 |
---|---|
[혼공컴운]5주차_Ch.12 - 13 (0) | 2025.02.18 |
[혼공컴운]4주차_Ch.09 - 11 (0) | 2025.02.13 |
[혼공컴운] 3주차_Ch.06 - 08 (0) | 2025.02.03 |
[혼공컴운] 1주차_Ch.01 - 03 (0) | 2025.01.12 |