컴퓨터 구조 + 운영체제 2주차 입니다. 이번 글에서는 CPU의 구성 요소인 ALU, 제어 장치, 레지스터와 명령어 사이클과 인터럽트, 빠른 속도의 CPU 설계를 위한 클럭과 코어, 스레드, 더 빠른 속도로 명령어를 실행하여 CPU를 굴리도록 하는 명령어 처리 기법인 파이프라인, 명령어 집합 ISA를 기반으로 설계된 CISC와 RISC에 대해 알아보도록 하겠습니다.
도서 정보
책 제목: 혼자 공부하는 컴퓨터 구조 + 운영체제
저자: 강민철
출판사: 한빛미디어
책 정보 및 구매 사이트: https://www.hanbit.co.kr/store/books/look.php?p_code=B9177037040
혼자 공부하는 컴퓨터 구조+운영체제
어려운 컴퓨터 구조와 운영체제의 원리를 누구나 쉽게 이해할 수 있도록 용어와 개념은 한 번 더 풀어쓰고, 적절한 예시와 이해하기 쉬운 그림으로 재미있게 구성했다. 또한 일상 소재를 활용한
www.hanbit.co.kr
저자의 유튜브 강의: https://www.youtube.com/playlist?list=PLYH7OjNUOWLUz15j4Q9M6INxK5J3-59GC
혼자 공부하는 컴퓨터구조 + 운영체제
www.youtube.com
목차
4. CPU의 작동 원리
4.1 ALU와 제어장치
4.2 레지스터
4.3 명령어 사이클과 인터럽트
5. CPU 성능 향상 기법
5.1 빠른 CPU를 위한 설계 기법
5.2 명령어 병렬 처리 기법
5.3 CISC와 RISC
부록 - 기본 숙제
추가 숙제 - 코어와 스레드, 멀티 코어와 멀티 스레드의 개념을 정리하기
키아 우수 혼공족 감사합니다!
4. CPU의 작동 원리
4.1 ALU와 제어장치
컴퓨터 구조의 지난 글에서 CPU는 ALU와 제어장치, 레지스터로 구성되어 있다고 설명하였습니다. 이 때 ALU는 연산 장치로써 계산을 담당하고, 제어 장치는 명령어를 해석하며, 레지스터는 프로그램을 실행하는데 필요한 값들을 임시로 저장한다고 하였습니다.
ALU
그 중 ALU에 대해서 먼저 알아보겠습니다. ALU가 하는 일은 무언가로부터 입력을 받아 연산을 하고, 연산한 값을 내보내는 역할을 합니다. ALU에게 일을 시키는 부품들은 어떤 것이고, 어떤 정보를 받으며, 누구에게 어떤 정보를 주는 지는 다음과 같습니다.
ALU는 제어 장치에게 어떠한 연산을 수행해야 하는지 알려주는 제어 신호를 전달 받고, 레지스터를 통해 연산해야 하는 피연산자를 제공 받습니다. ALU 내부에서 연산을 진행한 후 플래그 레지스터에게 연산 결과의 부가 정보를 담고 있는 플래그 값을 전달해주고, 레지스터에게 연산의 결괏값을 전달해줍니다.
ALU가 내보내는 정보에 대해 조금 더 자세히 알아보겠습니다. ALU가 연산한 결괏값은 숫자나 문자 혹은 메모리 주소가 될 수 있습니다. 또한 이 결괏값은 레지스터에 임시로 보관이 됩니다. 그렇다면 결괏값을 왜 굳이 메모리가 아닌 레지스터에 저장할까요? 그 이유는 CPU로부터 메모리보다 레지스터 까지의 물리적 거리가 비교적 가깝기 때문입니다. 거리가 가깝기 때문에 더 자주 정보를 참조하기 쉽고, 이는 프로그램의 실행 속도와 직접적인 연관이 있습니다.
플래그
ALU의 결괏값에는 연산 결과의 부가 정보인 플래그가 담겨 있다고 하였습니다. 플래그는 연산의 결괏값이 음수인지 양수인지, 연산 결과가 0인지, 오버플로우(연산 결과가 연산 결과를 담을 레지스터보다 큰 상황)가 발생했는지 등을 표현하는 정보입니다. 다음은 ALU가 표현하는 대표적인 플래그를 나타내는 표 입니다.
가장 대표적인 플래그들을 정리하여 보았습니다. 이 플래그들은 앞선 표에서 'ALU가 주는 것'에 해당하는 플래그 레지스터에 저장됩니다. 부호 플래그는 저번 컴퓨터 구조 1주차의 2.1 0과 1로 정보를 표현하는 방법에서 언급했던 '부호 비트'와 비슷한 개념이고, 제로 플래그는 피연산자를 통한 연산 결과가 0인지를 나타내는 플래그라고 하였습니다. 제로 플래그가 1일 경우 피연산자가 같은 값이라는 것을 알 수 있겠네요. (a - b = 0 이라면 a = b) 레지스터에는 플래그 레지스터, 명령어 레지스터 등이 있습니다. 레지스터에 관한 자세한 정보는 4.2 챕터에서 알아보겠습니다.
제어 장치
제어 장치는 제어 신호를 보내고, 명령어를 해석한다고 하였습니다. 제어 장치 역시 ALU와 마찬가지로 무언가로부터 정보를 전달받고, 제어 장치 내에서 처리한 정보를 각 부품에 전달합니다. 제어장치는 다음과 같은 특징이 있습니다.
------------------------------------------------------------------------------------------------------------
- 제어장치는 클럭 신호에 맞추어 동작한다.
- 제어장치는 명령어 레지스터로부터 해석해야 할 명령어를 전달받는다.
- 제어장치는 플래그 레지스터로부터 플래그 값을 전달받는다.
- 제어장치는 시스템 버스 중 제어 버스로 전달된 제어 신호를 전달받는다.
- 제어장치가 내보내는 제어 신호는 제어 버스를 통하여 CPU 내부 부품과 CPU 외부 부품(메모리, 보조기억장치, 입출력장치)에 전달한다.
------------------------------------------------------------------------------------------------------------
먼저 클럭(Clock)에 대하여 알아보겠습니다. 클럭이란 컴퓨터의 모든 부품을 일사불란하게 움직일 수 있도록 하는 시간 단위입니다. 전기적 진동의 속도 단위라고도 하며, 이 진동 역시 0과 1로 이루어져 있습니다. 클럭 신호의 한 주기에 맞추어 레지스터간 데이터가 이동 된다거나, ALU가 연산을 수행한다거나, CPU가 메모리에 저장된 명령어를 읽어 들이는 등, 컴퓨터의 각 부품들이 클럭 신호에 맞추어 작동합니다. 하지만 각 부품들이 매 진동마다 작동하는 것이 아니라, 여러 진동에 걸쳐 작동합니다. 예시를 들어보겠습니다.
위 사진은 저의 노트북 사양입니다. 붉은색 사각형 안에 있는 것이 클럭 속도입니다. 헤르츠(Hz)라는 단위를 쓰는데, 이는 1초에 나타나는 클럭 주기를 말하는 것입니다. 저의 노트북의 경우 3.20GHz로 초당 약 320,000,000번 반복되고 있습니다. 즉 컴퓨터의 부품들이 1초에 3억 2천만번 일을 반복하는 것이 아닌, 그 클럭 주기의 신호에 맞추어서 동작을 하는 것입니다.
4.2 레지스터
앞서 레지스터는 프로그램을 실행하는데 필요한 값들을 임시로 저장한다고 하였습니다. 그 필요한 값에는 특정 숫자나 문자 혹은 메모리 주소가 포함되어 있다고 하였는데요. 이번 절에는 레지스터의 종류와, 프로그램이 실행되면서 위 값들이 어떠한 방식으로 레지스터에 저장되는지 CPU와 메모리의 관계를 예시로 들어 설명해보겠습니다.
컴퓨터 구조의 지난 글에서 설명했던 대략적인 컴퓨터의 전체 부품 그림입니다. 위 그림에서 CPU 내부의 왼쪽을 보시면 기다란 직사각형이 여러개 붙어 있는 것을 알 수 있습니다. 이것들을 레지스터라고 하는데, 각 레지스터마다 하는 역할과 이름이 다릅니다. 하지만 그 레지스터도 CPU 제조사마다 기능과 역할이 다릅니다. 그러나 굉장히 중요한 역할을 맡고 있는 일부 레지스터는 여러 CPU에서도 공통으로 사용하고 있습니다. 다음은 그 특별한 레지스터 8가지를 이름과 함께 역할을 나열한 것입니다.
-----------------------------------------------------------------------------------------
1. 프로그램 카운터 (PC: Program Counter) - 메모리에서 읽어올 명령어의 주소를 저장한다.
2. 메모리 주소 레지스터 (MAR - Memory Address Register) - 메모리의 주소를 저장한다.
3. 메모리 버퍼 레지스터 (MBR - Memory Buffer Register) - 메모리와 주고받을 데이터와 명령어를 저장한다.
4. 명령어 레지스터 (IR - Instruction Register) - 메모리에서 읽어들인 명령어를 저장한다.
5. 플래그 레지스터 (Flag Register) - 연산 결과 혹은 CPU 상태와 같은 부가적인 정보를 저장한다.
6. 범용 레지스터 (General Purpose Register) - 일반적인 상황에서 자유롭게 사용하는 레지스터이다.
7. 스택 포인터 (Stack Pointer) - 스택의 꼭대기의 위치(주소)를 저장한다.
8. 베이스 레지스터 (Base Register) - 기준 주소, 프로그램의 시작 주소를 저장한다.
-----------------------------------------------------------------------------------------
프로그램 카운터와 메모리 주소 레지스터, 메모리 버퍼 레지스터와 명령어 레지스터의 역할이 비슷하여 햇갈리실 수도 있습니다. 그 둘의 각각의 세부적인 역할은 다음과 같습니다.
프로그램 카운터는 먼저 프로그램의 시작 주소를 먼저 저장합니다. 그 주소에서 볼 일이 끝나면(다른 레지스터에서 데이터를 읽어 왔거나, 명령어 실행이 끝나면) 프로그램 카운터가 증가되어 그 다음 메모리 주소를 읽어들일 준비를 합니다. 즉 어느 메모리 주소의 명령어를 읽어야 할지 알려주는 레지스터입니다.
그렇다면, 메모리 주소 레지스터는 CPU가 읽고자 하는 주소 값을 주소 버스로 보낼 때 사용하는 레지스터입니다. 주소 버스를 통해 메모리 버퍼 레지스터를 거치기 때문에 버스 정류장 정도로 이해하시면 될 것 같습니다.
메모리 버퍼 레지스터는 메모리와 주고받을 데이터와 명령어를 저장한다고 하였습니다. 이는 메모리에 쓰고 싶은 값이나 메모리로부터 전달받은 값이 메모리 버퍼 레지스터에 거쳐간다는 의미입니다. 메모리 주소 레지스터와 마찬가지로 이 레지스터에 들어오는 값들은 데이터 버스를 통하기 때문에 버스 정류장 비슷하게 생각하시면 될 것 같습니다.
반면에, 명령어 레지스터는 메모리에서 읽어들인 명령어를 저장하지만, 메모리 버퍼 레지스터와 다른점은 제어장치가 명령어 레지스터 속 명령어를 받고 해석한 뒤 제어 신호를 내보낸다는 점 입니다.
프로그램 카운터, 메모리 주소 레지스터, 메모리 버퍼 레지스터, 명령어 레지스터가 프로그램이 실행될 때 어떠한 방식으로 각 레지스터에 값이 저장되는지 CPU와 메모리 사이의 동작 방식을 예시로 설명하겠습니다. 글로만 설명하기엔 조금 복잡할 수 있으니 아래 과정을 읽으면서 따로 그려보시는걸 추천 드립니다(CPU, 시스템 버스, 메모리). 해당 예시는 혼자 공부하는 컴퓨터 구조 + 운영체제 p.114 ~ p.117에 나와 있습니다.
전제 - 프로그램의 시작주소가 메모리 주소의 1000번지 부터 1500번지에 저장되어 있습니다.
메모리 주소 1000번지에는 1101(2)라는 값이, 1001번지에는 1111(2)라는 값이 저장되어 있습니다.
1. 프로그램을 처음부터 실행하기 위해 프로그램 카운터에는 메모리 주소인 1000이 저장됩니다. 프로그램의 시작 주소가 1000번지 이고, 이는 메모리에서 가져올 명령어가 1000번지부터 시작한다는 뜻입니다.
2. 1000번지를 읽어야 하기 때문에 메모리 주소 레지스터에 1000이 저장됩니다. 1000번지 라는 값을 메모리에게 전달 해야 하기 때문에 주소 버스를 통하여 이 값이 전달됩니다.
3. 제어 장치에서 제어 버스를 통해 메모리에 제어 신호를 보냅니다. 동시에 메모리 주소 레지스터에서 주소 버스를 통해 메모리 주소를 메모리에 전달합니다.
4. 메모리 1000번지에 저장된 값인 1101(2)은 데이터 버스를 통해 메모리 버퍼 레지스터로 전달됩니다. 이후 프로그램 카운터는 증가되어 다음 명령어를 읽을 준비를 합니다. 즉 프로그램 카운터에 들어있는 값은 1000에서 1001로 증가하게 됩니다.
5. 메모리 버퍼 레지스터에 저장된 값은 명령어 레지스터로 이동합니다.
6. 제어장치는 명령어 레지스터의 명령어를 읽고 해석하며 제어 신로를 발생시킵니다.
위 과정이 프로그램의 실행을 위해 메모리 주소를 순차적으로 읽어들이는 과정입니다. 4번째 순서에서 프로그램 카운터가 증가한 것을 보아 프로그램 카운터는 지속적으로 증가하여 계속해서 프로그램을 실행해 나가는 것을 알 수 있습니다.
4.3 명령어 사이클과 인터럽트
명령어 사이클
프로그램은 순차적인 흐름으로 동작합니다. CPU는 그 흐름이 안정적으로 돌아갈 수 있도록 명령어들을 반복하여 처리합니다. 이렇게 하나의 명령어를 처리하는 흐름을 명령어 사이클이라고 합니다. 명령어 사이클은 인출 사이클과 실행 사이클으로 이루어집니다. 인출과 실행 과정 사이 에서 메모리 접근이 한번 더 필요한 경우가 있습니다. 이러한 상황을 간접 사이클이라고 합니다. 인출 사이클은 바로 위 프로그램이 실행되기 위해 명령어를 읽어들이는 과정의 5번 까지가 인출 사이클에 해당합니다. 실행 사이클은 위 과정의 6번 과정에 해당합니다.
인터럽트
인터럽트는 영단어 interrupt의 의미 그대로 '방해하다'라는 뜻 입니다. 즉 CPU의 작업을 방해하는 신호입니다. CPU가 작업을 중단해야 할 만큼 중요한 인터럽트는 동기 인터럽트(Synchronous Interrupts)와 비동기 인터럽트로 나뉩니다.
동기 인터럽트는 '예외(exception)'라고도 하며 CPU에 의해 발생하는 인터럽트입니다. 프로그래밍 상의 오류와 같은 상황에서 발생합니다.
비동기 인터럽트는 대부분 '하드웨어 인터럽트(Hardware Interrupts)'라고 부릅니다. 이 상황은 에를 들어 프린터기에 입출력 작업을 지시하면 작업을 끝낸 프런터기가 CPU에 완료 알림(인터럽트)를 보냅니다. 또한 키보드나 마우스같은 입력장치가 입력을 받았을 때 이를 처리하기 위해 CPU에게 입력 알림(인터럽트)를 보냅니다. 위에서 설명드렸던 인터럽트 플래그가 여기서 사용됩니다. 입출력장치가 CPU에게 인터럽트 요청 신호를 보내면 CPU는 이를 확인하고 인터럽트 플래그를 통해 인터럽트를 받을 수 있는지 확인합니다. 인터럽트를 받아들일 수 있다면, 인터럽트 플래그가 1이라면 CPU는 작업을 백업합니다. 인터럽트를 처리하면 백업해 둔 작업을 복구하여 실행을 재개합니다.
5. CPU 성능 향상 기법
5.1 빠른 CPU를 위한 설계 기법
지금까지는 CPU가 명령어를 처리하는 과정에 대하여 알아보았습니다. 이번 절에는 이러한 과정들을 어떻게 하면 더 빠르게 반복시킬 수 있는지에 대하여 설명하겠습니다.
클럭
클럭은 앞서 설명한 개념과 제어장치에서 설명한 개념과 완벽히 동일합니다. 클럭은 컴퓨터 부품을 움직이도록 하는 시간 단위(전기적 진동 단위)라고 하였습니다. 부품들이 매 클럭 주기에 맞추어 동작하는 것이 아닌 여러 클럭 신호에 걸쳐 동작한다고 하였습니다. 그렇다면 클럭 주기가 빠를 수록 CPU의 처리 속도가 더 빨라집니다. 그 예시로 위 사진에서 저의 노트북의 클럭 속도는 3.20(GHz)로 초당 약 320,000,000번 클럭 주기가 반복된다고 하였습니다.
하지만 클럭 속도가 3.20GHz로 매 시간 일정한 것은 아닙니다. 위 사진의 그래프는 클럭 속도에 따른 시간 그래프 입니다. 여러분들은 게임이나 포토샵 등을 하면서 컴퓨터가 뜨거워지거는 현상을 겪어보신 적이 있으실 것입니다. 게임과 같은 고성능의 작업을 필요로 할 경우 CPU는 자체적으로 클럭 속도를 높이고, 그것이 컴퓨터의 발열을 지속시키는 이유입니다. 반면 아무 작업도 하지 않거나, 비교적 가벼운 프로그램을 실행할 때 컴퓨터는 의도적으로 클럭 속도를 낮춥니다. 하지만 클럭 속도를 높인다고 해서 CPU의 성능이 좋아지는 것은 아닙니다.
코어와 멀티코어
위 사진에서 아래쪽에 작은 글씨로 기본 속도: 3.20GHz, 소켓: 1, 코어: 8, 논리 프로세서: 16 ... 와 같이 나타내고 있습니다. 저의 노트북의 경우에는 8코어라고 되어 있습니다. '명령어를 실행하는 부품'으로써 과거에는 하나만 존재했습니다. 그러나 현재에 이르러 CPU 내부에는 '명령어를 실행하는 부품'을 여러 개 연결할 수 있게 되었습니다. 즉 위 컴퓨터의 대략적인 구조를 그려놓은 그림에서 하나로써 존재하는 CPU는 오늘날의 '코어'가 되었습니다. 그리고 오늘날의 CPU는 이 '코어'를 두개 혹은 네개 이어놓은 형태가 되었는데 이를 각각 '듀얼코어', '쿼드코어'라고 부릅니다. CPU가 많을 수록 명령어 처리 속도도 빨라질테고, 이는 컴퓨터의 성능에 중요한 요인 중 하나입니다. 하지만 이 코어를 100개 이어붙인다고 해도 컴퓨터의 성능이 좋아지지는 않습니다. 종이학 1000마리를 접어야 하는데 같이 접을 친구 5만명을 데려온 것이라고 이해하시면 좋을 것 같습니다.
스레드와 멀티스레드
클럭 속도, 코어의 개수 외에도 컴퓨터의 성능을 결정짓는 중요한 요소는 이외에도 더 있습니다. 그 중 하나는 '스레드(Thread)'로 스레드는 '실행 흐름의 최소 단위'라는 사전적 의미를 담고 있으나, 이를 더 정확하게 표현하여 하드웨어적 스레드와 소프트웨어적 스레드로 분류할 수 있습니다.
먼저 하드웨어적 스레드에 대해 설명해 드리겠습니다. 하드웨어적 스레드의 정의는 하나의 코어가 동시에 처리하는 명령어 단위입니다. 우리는 지금까지 알아본 CPU는 1코어 1스레드로 구성된 CPU의 형태였습니다. 이는 명령어를 실행하는 부품이 한 개 있고, 한 번에 하나씩 명령어를 처리한다는 의미입니다. 하지만 현재에는 한 코어 내에 두개의 스레드를 담을 수 있습니다. 이를 1코어 2스레드 라고 하는데, 하나의 코어 안에 명령어를 실행하는 부품이 두 개 있고, 한 번에 두 개씩 명령어를 처리한다는 의미입니다. 이처럼 하나의 코어로 여러 명령어를 동시에 처리하는 CPU를 멀티스레드 프로세서(Multithread Processer) 혹은 멀티스레드 CPU라고 합니다. 하드웨어적 스레드를 논리 프로세서라고도 합니다. 위 작업 관리자의 성능 탭에서 제 노트북은 8개의 코어에 16개의 스레드가 있는 것입니다. 이를 8코어 16스레드 라고 합니다.
다음으로 소프트웨어적 스레드에 대해 설명해 드리겠습니다. 소프트웨어적 스레드의 정의는 하나의 프로그램에서 독립적으로 실행되는 단위입니다. 소프트웨어적 스레드를 접할 수 있는 상황은 여러분들이 프로그래밍 언어나 운영체제를 학습할 때 접하게 될 것입니다. 프로그램은 거대한 흐름으로 이루어져 있습니다. 이 거대한 흐름에서 한 부분만 실행시킬 수도 있겠지만, 한 프로그램 내에서 동시에 여러 부분을 실행해야 할 수도 있습니다. 워드(Word) 프로그램을 예시로 들어보곘습니다. 워드는 사용자로부터 입력받은 내용을 화면에 출력해줍니다. 그와 동시에 입력한 내용이 맞춤법에 맞는지 표시해주는 기능도 있습니다. 그 동시에 사용자가 입력한 내용을 저장까지 해 줍니다. 이러한 내용들을 작동시키는 코드를 각각의 스레드로 만든다면 동시에 실행시킬 수 있습니다.
하지만 1코어 1스레드 CPU로도 소프트웨어적 스레드를 수십개 실행하여 프로그램의 여러 부분을 동시에 실행할 수도 있습니다. 이러한 개념이 하드웨어적 스레드와 혼동이 될 수 있기 때문에 하드웨어적 스레드와 소프트웨어적 스레드의 개념을 잘 구분하고 숙지하여 넘어가는 것이 좋습니다.
5.2 명령어 병렬 처리 기법
지금까지는 CPU의 성능을 높이는 방법에 대하여 알아보았습니다. 이번에는 CPU의 효율을 높이는 방법에 대하여 설명하겠습니다. CPU를 효율적으로 쓰기 위한 예시로는 명령어를 동시에 처리하도록 하는 기술이 있습니다. 이를 명령어 병렬 처리 기법((ILP - Instruction-Level Parallelism)이라고 합니다. 명령어 병렬 처리 기법에는 대표적으로 명령어 파이프라이닝, 슈퍼스칼라, 비순차적 명령어 처리가 있습니다.
명령어 파이프라인
명령어들을 동시에 처리한다고 해서 두개 이상의 명령어를 시작부터 동시에 처리하는 것은 아닙니다. 앞선 명령어 처리 과정을 클럭 단위로 정리하면 명령어 인출 --> 명령어 해석 --> 명령어 실행 --> 결과 저장의 순서로 이루어집니다. CPU는 이 과정 중 같은 단계가 겹치지 않는 한 각 단계를 동시에 실행할 수 있습니다. 음악에서 돌림노래처럼 첫 번째 명령어를 인출하고, 그 다음으로 해석할 때 두번째 명령어를 인출합니다. 첫 번째 명령어의 해석이 끝나고 실행 단계에 접어들 때 두 번째 명령어는 해석 단계에 접어들고, 세 번째 명령어가 인출되는 방식입니다. 이와같이 명령어들을 명령어 파이프라인에 넣고 동시에 처리하는 기법을 명령어 파이프라이닝(Instruction Pipelining)이라고 합니다. 현대에는 단일 파이프라인이 아닌 다중 파이프라인을 사용합니다. 이를 슈퍼스칼라(superscalar)라고 하는데, 슈퍼스칼라는 파이프라이닝 기법을 동시에 여러개를 사용하는 기술입니다. 쉽게 말하여 단일 파이프라인은 4개의 마디로 구성된 돌림노래를 부르는 사람이 한명씩 있다면 다중 파이프라인은 돌림노래를 부르는 사람이 각 마디마다 둘 이상 있다고 생각하시면 좋을 것 같습니다.
하지만 파이프라이닝 기법이 항상 효율적인 것만은 아닙니다. 파이프라이닝은 명령어를 동시에 처리하는 기법입니다.만약 동시에 하나의 메모리에 접근하거나, 프로그래밍에서 if문과 같은 프로그램의 실행이 분기하는 상황에서 뜻하지 않은 오류가 발생할 수 있습니다. 이를 파이프라인 위험이라고 하는데, 파이프라인 위험은 데이터 위험, 제어 위험, 구조적 위험으로 크게 세 가지로 나뉩니다.
데이터 위험(Data Hazard)은 다음에 실행할 명령어가 현재 실행하는 명령어의 연산 결과를 필요로 하는 경우에 발생됩니다. 예시를 들어보겠습니다.
--------------------------------------------------
명령어 1: a = b + c
명령어 2: d = e + a
--------------------------------------------------
명령어 1에서 a의 값이 나오려면 명령어가 인출되고, 해독되고, 실행되고, 저장되어야 비로소 a의 값이 나옵니다. 그러나 명령어 1이 인출되고 해독되어야 할 타이밍에 명령어 2가 인출됩니다. 명령어 1의 명령어 사이클이 끝나기 전에 명령어 2에서 명령어 1의 연산 결괏값을 필요로 하기때문에 이를 억지로 실행하려고 하면 파이프라인에 오류가 생깁니다. 이러한 상황을 데이터 위험이라고 합니다.
이를 해결하기 위한 기술로 비순차적 명령어 처리 기법(OoOE - Out of order execution) 이 있습니다. 데이터 위험이 발생하게 되는 상황은 '데이터 의존성'에 의해 발생합니다. 두 개 이상의 명령어가 서로 간의 연산 결괏값에 의존하여 실행되기 때문에 데이터 위험이 발생하게 되는 것입니다. 지금까지 배웠던 스레드, 명령어 병렬 처리 기법은 코어가 놀지 않도록, 쉼없이 굴리기 위하여 만들어진 기술입니다. 명령어 실행 중 데이터 위험 요소가 발견되면 명령어 사이클을 완전히 돌리기 전 까지 파이프라이닝을 온전히 사용할 수 없습니다. 돌림노래 처럼 처리되어야 할 명령어들이 중간에 멈추게 되는 것입니다. 그렇기 때문에 명령어 의존성에 얽매이지 않는 나머지 명령어들과 순서를 바꾸어, 문제가 되는 명령어의 사이클을 돌리는 동안 나머지 명령어들을 미리 실행하는 기법이 비순차적 명령어 처리 기법입니다.
제어 위험(Control Hazard)은 앞서 언급했던 if문과 같은 분기 상황에서 발생하는 위험입니다. 보통 프로그램에서 분기의 형태는 조건이 맞으면 분기하고 아니면 분기하지 않습니다.하지만 분기의 조건을 수행하는 명령어의 실행이 끝나지도 않았는데 어떻게 프로그램이 분기할 수 있을까요?
제어 위험은 분기 상황 외에도 함수 호출 상황에서 발생할 수 있습니다. 만약 CPU가 프로그램을 흐름대로 처리하고 있다가 어떤 코드를 만났습니다. 이 코드를 해독하고 실행하다보니 하필이면 이 코드가 함수를 호출하는 명령어 였네요. 결국 CPU는 함수를 선언헀던 명령어로 돌아가야 하는 경우가 있습니다. 이 경우 명령어 파이프라인에 의해 먼저 처리되고 있던 명령어들은 아무 의미가 없게 됩니다. CPU가 헛수고 한 것이죠. 이러한 상황을 위해 '분기 예측(Branch Prediction)'이라는 기술이 등장하였습니다. 분기 예측은 프로그램이 언제 어디서 분기할지 사전 정보에 의해 예측하고, 메모리 주소나 명령어를 미리 인출하는 기술입니다.
마지막으로 구조적 위험(Structure Hazard)입니다. 구조적 위험은 앞서 언급했던 하나의 메모리에 두 개 이상의 명령어가 접근할 때 발생합니다. 구조적 위험은 자원 위험(Resource Hazard)라고도 불리며, 폰 노이만 구조에서 하나의 메모리에서 두 개 이상의 접근은 구조적으로 불가능하기 때문에 구조적 위험으로 불리게 되는 것입니다.
5.3 CISC와 RISC
지금까지 CPU의 성능을 끌어올리고 효율을 높일 방법에 대하여 알아보았습니다. 하지만 이것들은 전부 명령어가 있어야 실현이 가능한 기술들입니다. 게다가 코어, 스레드, 명령어 병렬 처리 기법이라는 대단한 기술들도 만들었는데, 이렇게 대단한 기술들도 있겠다, 이왕 명령어도 이 기술에 적용시키기 쉽게 생긴다면 더더더 CPU의 효율이 극대화 될 것입니다.
명령어 집합 구조(ISA)
명령어는 생각보다 대단한 친구입니다. 명령어의 생김새에 따라 CPU의 성능이 올라가는 것 뿐만 아니라 CPU가 어떤 명령어를 이해하느냐에 따라 컴퓨터의 구조와 설계 방식마저 달라지기 때문입니다. (혹은 반대의 경우일 수도 있겠지만) 저번 컴퓨터 구조 글에서 명령어는 CPU의 제조사에 따라 다르다고 하였습니다. 인텔 사( 社 )CPU의 명령어, ARM 사( 社 )CPU의 각각의 제조사의 CPU가 이해할 수 있는 명령어들의 모음을 명령어 집합 구조(ISA - Instruction Set Architecture)라고 합니다. 명령어 집합 구조를 앞으로 ISA라고 칭하겠습니다. ISA는 CPU 제조사마다 다르다고 하였습니다. 어떻게 다른지 x86-64와 ARM의 명령어를 기계어로 비교해보도록 하겠습니다. 순서대로 원본 소스코드, x86-64의 기계어, ARM의 기계어 입니다. 아래 코드들은 혼자 공부하는 컴퓨터 구조 + 운영체제 p.168쪽 내용을 발췌하였으며, 동일한 버전의 컴파일러를 사용하였습니다.
//Origin Source Code.C
#include <stdio.h>
int main(void) {
int a = 1;
int b = 2;
int c = a + b;
return 0;
}
//x86-64 machine lan.asm
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 1
mov DWORD PTR [rbp-8], 2
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
mov DWORD PTR [rbp-12], eax
mov eax, 0
pop rbp
ret
//ARM machine lan.asm
push {r7}
sub sp, sp, #20
add r7, sp, #0
movs r3, #1
str r3, [r7, #12]
movs r3, #2
str r3, [r7, #8]
ldr r2, [r7, #12]
ldr r3, [r7, #8]
add r3, r3, r2
str r3, [r7, #4]
movs r3, #0
mov r0, r3
adds r7, r7, #20
mov sp, r7
ldr r7, [sp], #4
bx lr
위와 같이 같은 소스 코드일지라고 CPU에 따라 다른 기계어의 형태를 하고 있습니다. 다른 기계어를 사용함으로써 각각의 특징과 장단점은 다음과 같습니다.
CISC
CISC(Complex Instruction Set Computer)는 영단어의 뜻 그대로 복잠한 명령어 집합입니다. 이 명령어 집합을 사용하는 대표적인 ISA는 x86-64입니다. CISC는 복잡한 명령어를 사용하는 만큼 사용자에게 초점이 맞추어져 있습니다. CISC의 특징으로는 복잡한 만큼 강력하고, 다양한 종류의 명령어를 제공합니다. 또한 명령어의 길이를 자유자재로 설정할 수 있습니다(가변 길이 명령어). 메모리 접근 방식도 다양해서 특정 상황에 쓰이는 메모리 접근 방식들도 있습니다. CISC의 가장 큰 장점으로는 앞선 특징들을 조합하여 전체 명령어의 길이가 짧다는 점입니다.
하지만 이 장점은 옛날 슈퍼마리오를 40KB로 만들 때나 유효한 특징입니다. 현재에는 용량이 아주 널널하기 때문에(게임 하나가 70GB가 넘어가는) 이러한 장점은 크게 부각되지 않는다는 점입니다. 또한 CISC에는 치명적인 단점이 몇 있습니다. CISC가 강조하는 복잡하고 다양한 명령어를 제공하지만, 어차피 사용자는 사람입니다. 프로그램을 개발할 때 기분따라 명령어를 쓰는 것이 아닌 쓰던거만 주구장창 쓰는게 인간 개발자이지요. 그렇기 때문에 복잡하고 다양한 명령어 역시 큰 장점이 아니라는 것입니다. 또한 복잡하고 다양한 명령어 때문에 클럭 주기를 많이 잡아먹게 되고, 가변 길이 명령어의 특징으로 명령어의 크기 때문에 실행 시간이 일정하지도 않습니다. 이는 파이프라이닝을 어렵게 만듭니다.
이러한 CISC의 단점과 한계가 RISC를 낳았습니다.
RISC
현재 사회에서 사람들은 CISC의 치명적인 단점들을 보고 원활한 파이프라이닝을 위한 고정 길이 명령어의 중요성과, 어차피 명령어를 쓰던것만 쓴다는 사실을 깨우쳤습니다. 이러한 사실들을 인정하고 새로운 명령어 집합을 만든 결과 RISC(Reduced Instruction Set Computer)가 탄생하였습니다. 영단어의 뜻 그대로 명령어를 줄였습니다. 그 결과 RISC는 명령어 파이프라이닝에 최적화된 명령어 집합이 되었습니다. 1클럭 내외로 실행되기 때문에 각 명령어 사이클을 빠르고 효율적으로 돌리기에 최적화 된 것입니다.
CISC에는 메모리에 접근하는 명령어가 다양하다고 하였습니다. 그래서 RISC에는 메모리에 접근하는 명령어를 load와 store 단 두개로 줄였습니다. 그러나 RISC는 메모리 접근을 최소화 함과 동시에 레지스터를 더 많이 활용합니다. 그렇기 때문에 CISC에 비해 범용 레지스터를 더욱 많이 사용합니다. 위 RISC의 어셈블리 코드에서 r1, r2, r3 ... 이 레지스터 이름입니다.
CISC와 RISC의 특징을 정리하며 이번 글을 마치겠습니다.
------------------------------------------------------------
CISC
- 다양하고 복잡한 명령어를 제공한다.
- 가변 길이 명령어를 제공한다.
- 프로그램 전체의 명령어 길이가 짧다.
- 명령어 파이프라이닝이 어렵다.
RISC
- 단순하고 적은 수의 명령어를 제공한다.
- 고정 길이 명령어를 제공한다.
- 프로그램 전체의 길이가 길다.
- 명령어 파이프라이닝이 쉽다.
------------------------------------------------------------
부록
추가 숙제는 5.1 빠른 CPU를 위한 설계 기법에서 코어와 멀티코어, 스레드와 멀티 스레드의 개념을 설명하였기에 넘어가겠습니다.
기본 숙제
혼자 공부하는 컴퓨터 구조 + 운영체제 p.125 확인문제 2번, p.155의 확인문제 4번 풀고 인증하기
먼저 p.125 2번입니다.
설명에 맞는 레지스터를 보기에서 찾아 빈칸을 채워 보세요
보기 - 프로그램 카운터, 명령어 레지스터, 플래그 레지스터, 범용 레지스터
(1): 연산 결과 혹은 CPU 상태에 대한 부가 정보를 저장하는 레지스터
(2): 메모리에서 가져올 명령어의 주소를 저장하는 레지스터
(3): 데이터와 주소를 모두 저장할 수 있는 레지스터
(4): 해석할 명령어를 저장하는 레지스터
정답
(플래그 레지스터): 연산 결과 혹은 CPU 상태에 대한 부가 정보를 저장하는 레지스터
(프로그램 카운터): 메모리에서 가져올 명령어의 주소를 저장하는 레지스터
(범용 레지스터): 데이터와 주소를 모두 저장할 수 있는 레지스터
(명령어 레지스터): 해석할 명령어를 저장하는 레지스터
풀이과정은 문제와 윗 글에서 이미 나와있기 때문에 추가로 작성하진 않겠습니다. 하지만 범용 레지스터에 대해 조금 더 알아보겠습니다.
x86-64의 어셈블리 언어에서 범용 레지스터는 위 어셈블리 코드에서 짐작 하셨듯이 eax, edx가 범용 레지스터입니다. 과거에는 eax가 가장 큰 크기의 레지스터였지만, 현재에는 rax가 가장 8바이트로 큰 레지스터 크기입니다. rax에서 절반을 자르면 eax, eax에서 또 반을 자르면 ax, ax에서 상위 8비트와 하위 8비트는 각각 AH, AL로 나뉩니다.
RAX - 64비트(8바이트)
EAX - 32비트(4바이트)
AX - 16비트(2바이트)
AH - 상위 8비트(1바이트)
AL - 하위 8비트(1바이트)
x86-64의 범용 레지스터에서 RAX, RBX, RCX, RDX, RDI, RSP 등 하는 역할과 쓰임새 모두 다릅니다. 이들에 대한 자세한 설명은 아키텍쳐 혹은 어셈블리 부분에서 추후에 다루겠습니다.
p.155 확인문제 4번
다음 빨간 상자 안에 들어갈 단어를 써 넣으시오.

정답: 코어
이전 컴퓨터 구조 글에서는 저 코어 하나가 CPU라고 하였습니다. 하지만 현재에서 CPU는 코어가 되었고 현재의 CPU는 이 코어들을 두 개, 네 개 연결해 놓은 형태가 되었다고 설명하였습니다.
'컴퓨터 구조 + 운영체제 > 컴퓨터 구조' 카테고리의 다른 글
혼자 공부하는 컴퓨터 구조 + 운영체제 3주차 (메모리, 캐시, 보조기억장치, RAID, 입출력장치) (1) | 2024.07.22 |
---|---|
혼자 공부하는 컴퓨터 구조 + 운영체제 1주차 (컴퓨터 구조, 데이터, 명령어, 진수, 스택과 큐) (0) | 2024.07.08 |