🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
CS
컴퓨터구조
05-CPU 성능 향상기법

1. 빠른 CPU를 위한 설계 기법

간혹 PC방에서도 고사양 게임을 즐길 수 있다는 점을 부각하기 위해 광고 전단지나 입간판에 이런 용어들을 사용하곤 하지요.

  • 클럭
  • 코어 & 멀티코어
  • 스레드 & 멀티스레드
  • e.g. 무슨무슨 PC방
    • 전 좌석 멀티코어 CPU
    • 8코어 16스레드를 지원하는 CPU

1.1 클럭

MySelf-Architec-Oper_4_6

여러분이 CPU를 설계하는 엔지니어라고 가정해 봅시다. 여러분이 해야 할 일은 조금이라도 더 빠른 CPU를 만드는 일입니다. 그렇다면 CPU를 어떻게 설계해야 할까요?

  1. 컴퓨터 부품들은 ‘클럭 신호’에 맞춰 일시분란하게 움직인다.
  2. CPU는 ‘명령어 사이클’이라는 정해진 흐름에 맞춰 명령어들을 실행한다.

클럭 신호가 빠르게 반복되면, CPU를 비롯한 컴퓨터 부품들은 그만큼 빠른 박자에 맞춰 움직이겠죠? 즉, 클럭 속도가 높아지면 CPU는 명령어 사이클을 더 빠르게 반복할 것이고, 다른 부품들도 그에 발맞춰 더 빠르게 작동할 것입니다

실제로 클럭 속도가 높은 CPU일반적으로 성능이 좋습니다. 그래서 클럭 속도CPU 속도 단위로 간주되기도 합니다.

클럭 속도는 헤르츠(Hz) 단위로 측정합니다.

  • 헤르츠(Hz) : 1초에 클럭이 몇 번 반복되는지를 의미
  • e.g. 클럭이 ‘똑딱’하고 1초에 한 번 반복되면 CPU 클럭 속도는 1Hz
  • e.g. 클럭이 1초에 100 번 반복되면 CPU 클럭 속도는 100Hz

MySelf-Architec-Oper_5_1

실제 CPU 클럭 속도는 어떤지 볼까요? 그림 속 CPU를 보면 기본 속도(Base)는 2.5GHz, 최대 속도(Max)는 4.9GHz라는 것을 알 수 있습니다.

  • 이는 1초에 클럭이 기본적으로 25억(2.5×1092.5 × 10^9)번,
  • 순간적으로 최대 49억(4.9×1094.9×10^9)번 반복된다는 것을 나타냅니다.
  • cf. 1GHz1,000,000,000(109)1,000,000,000(10^9)Hz

💡 클럭 속도는 일정하지 않다 : 오버클럭킹(overclocking)

‘클럭’이라는 단어만 보고 시계를 떠올려 클럭 속도가 매번 일정하게 유지된다고 생각할 수도 있지만, 실제로는 그렇지 않습니다.

CPU 사진을 다시 보면 기본 클럭 속도(Base)와 최대 클럭 속도(Max)로 나뉘어 있죠? 이처럼 CPU는 계속 일정한 클럭 속도를 유지하기보다는 고성능을 요하는 순간에는 순간적으로 클럭 속도를 높이고, 그렇지 않을 때는 유연하게 클럭 속도를 낮추기도 합니다.

최대 클럭 속도를 강제로 더 끌어올릴 수도 있는데, 이런 기법을 오버클럭킹(overclocking)이라고 합니다.

그럼 클럭 속도를 무지막지하게 높이면 무조건 CPU가 빨라질까요? 안타깝게도 그렇지는 않습니다.

그래픽이 많이 요구되는 게임이나 영상 편집과 같은 CPU에 무리가 가는 작업을 장시간 하면, 컴퓨터가 뜨겁게 달아오르는 것을 경험해 본 적이 있을 겁니다. 클럭 속도를 무작정 높이면, 이러한 발열 문제가 더 심각해집니다.

이처럼 클럭 속도를 높이는 것은 분명 CPU를 빠르게 만들지만, 클럭 속도만으로 CPU의 성능을 올리는 것에는 한계가 있습니다.

그렇다면 클럭 속도를 늘리는 방법 이외에는?

  • 코어 수를 늘리는 방법 (e.g. ‘듀얼 코어’, ‘멀티코어‘)
  • 스레드 수를 늘리는 방법 (e.g. ‘멀티 스레드’)

1.2 코어와 멀티 코어

MySelf-Architec-Oper_5_1

다시 CPU 사진을 보면, 왼쪽에 코어(8코어)스레드(16스레드)가 나와있습니다. intel core i7-11700에서 ‘코어’라는 용어는 인텔 제품 이름입니다. 사진 하단의 Core, Thread, Max, Base 항목 중 Core 항목을 봐주세요.


1.2.1 코어(core)

MySelf-Architec-Oper_5_2

  • 현대적인 관점에서 CPU라는 용어를 재해석해야 함
  • CPU를 ‘명령어를 실행하는 부품’?
  • 전통적으로 ‘명령어를 실행하는 부품’은 원칙적으로 하나만 존재
  • But 오늘날 CPU에는 ‘명령어를 실행하는 부품’이 여러 개가 존재
    • 즉, 오늘날 CPU 내부에는 ‘명령어를 실행하는 부품’을 얼마든지 만들 수 있게 됨
  • **‘명령어를 실행하는 부품’**을 코어라는 용어로 사용
  • 다시 말해, 오늘날의 CPU는 단순히 **‘명령어를 실행하는 부품’**에서
    • **‘명령어를 실행하는 부품을 여러 개 포함하는 부품’**으로 명칭의 범위가 확장되었습니다.
  • CPU 사진에서 8코어‘명령어를 실행하는 부품’을 8개 포함하고 있다고 보면 됩니다

1.2.2 멀티코어(multi-core)

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)
  • 코어를 여러 개 포함하고 있는 CPU
    • 멀티코어(multi-core CPU) 또는 멀티코어 프로세서라고 부릅니다.
  • 이는 CPU 내에 명령어를 처리하는 일꾼이 여러 명 있는 것과 같습니다.
  • 당연히 멀티코어의 처리 속도는 단일코어보다 더 빠르겠죠?
  • e.g. 클럭 속도가 2.4GHz인 단일 코어 CPU클럭 속도가 1.9GHz인 멀티코어 CPU를 비교하면,
    • 일반적으로 후자의 성능이 더 좋습니다.

그럼 코어를 2개, 3개, 100개로 늘리면 연산 처리 속도도 2배, 3배, 100배로 빨라질까요?

  • 안타깝게도 CPU의 연산 속도가 꼭 코어 수에 비례하여 증가하지는 않습니다.
  • 학교에서 4인 1조로 조별 과제할 때를 생각해 볼까요?
    • 모두 똑같이 참여하여, 생산성의 네 배에 가까운 결과물을 만들어 내는 경우도 있으나,
    • 그렇지 않은 경우도 많습니다.
  • 업무가 균등하게 분배되지 않거나 한 두 사람만 열심히 참여하면,
    • 결과적으로 한 두 사람만의 생산성만큼 결과물이 나오게 되죠.

이처럼 코어마다 처리할 연산이 적절히 분배되지 않는다면, 코어 수에 비례하여 연산 속도가 증가하지 않습니다.

또한 처리하고자 하는 작업량보다 코어 수가 지나치게 많아도 성능에는 크게 영향이 없습니다.

  • 100인분 도시락은 1명이 만드는 것보다 10명이 만드는 것이 10배가량 빠르겠지만,
  • 4인분 도시락은 10명이 만드는 것이 5명이 만드는 것보다 특별히 더 빠르지 않은 것과 같습니다.

중요한 것은 코어마다 처리할 명령어들을 얼마나 적절하게 분배하느냐이고, 그에 따라서 연산 속도는 크게 달라집니다.


1.3 스레드와 멀티스레드

CPU의 멀티스레드 기술을 이해하려면 우선 ‘스레드’라는 용어를 정확히 이해해야 합니다. 스레드는 프로그래밍 언어를 학습할 때도 등장하고, 추후 운영체제를 학습할 때(10장)도 등장하며, CPU를 학습할 때(05장)도 등장합니다.

  • 스레드(thread)의 사전적 의미는 **‘실행 흐름의 단위’**입니다.
  • 하지만 이 정의를 활자 그대로 받아 들이지 말고 더욱 엄밀하게 이해해야 합니다.
  • CPU에서 사용되는 스레드프로그래밍에서 사용되는 스레드는 용례가 다르기 때문이지요.

이렇게 기억하시기 바랍니다. 스레드에는 2종류가 있습니다.

  • 스레드(thread)
    • CPU에서 사용되는 하드웨어적 스레드
    • 프로그램에서 사용되는 소프트웨어적 스레드

지금부터 하드웨어적 스레드와 소프트웨어적 스레드가 어떻게 다른지 설명하겠습니다.


1.3.1 하드웨어적 스레드

  • 하드웨어적 스레드 : 하나의 코어가 동시에 처리하는 명령어 단위

MySelf-Architec-Oper_5_3

  • 하나의 코어한 번에 하나의 명령어를 받아들여서, 한 번에 하나의 명령으로 실행할 수 있다면,
  • 1코어 1스레드 CPU입니다.
(1) 멀티 스레드

MySelf-Architec-Oper_5_4

  • 반면, 여러 스레드를 지원하는 CPU하나의 코어로도 여러 개의 명령어를 동시에 실행할 수 있습니다.
  • e.g. 2코어 4스레드 CPU
    • 명령어를 실행하는 부품을 두 개 포함하고, 한 번에 네 개의 명령어를 처리할 수 있는 CPU를 의미
  • 이처럼 하나의 코어로 여러 명령어를 동시에 처리하는 CPU를
    • 멀티스레드(multithread) 프로세서 또는 멀티스레드 CPU라고 합니다.

MySelf-Architec-Oper_5_1

아까 전 CPU 사진을 다시보면, 8코어 16스레드죠?

  • 이는 명령어를 실행하는 부품을 8개 포함하고, 한 번에 16개의 명령어를 처리할 수 있는 CPU를 의미합니다.
  • 이는 코어 하나당 두 개 의 하드웨어 스레드를 처리한다는 뜻으로도 볼 수 있습니다.

💡 하이퍼스레딩(hyper-threading)

멀티스레드와 함께 자주 접하는 용어로 하이퍼스레딩(hyper-threading)이라는 용어도 있습니다. 이는 인텔의 멀티스레드 기술을 의미합니다. 인텔이 자신들의 멀티스레드 기술에 하이퍼스레딩이라는 명칭을 부여한 것이지요.


1.3.2 소프트웨어적 스레드

  • 소프트웨어적 스레드 : 하나의 프로그램에서 독립적으로 실행되는 단위
  • 프로그래밍 언어운영체제를 학습할 때 접하는 스레드보통 소프트웨어적으로 정의된 스레드를 의미
  • cf. Python, Java, C++ 등의 프로그래밍 언어를 이용해 소프트웨어적 스레드를 만들 수 있습니다.

MySelf-Architec-Oper_5_5

하나의 프로그램은 실행되는 과정에서

  • 한 부분만 실행될 수도 있지만, ( = 싱글 스레드 )
  • 프로그램의 여러 부분이 동시에 실행될 수도 있습니다. ( =멀티 스레드 )

MySelf-Architec-Oper_5_6

예를 들어, 여러분이 워드 프로세서 프로그램을 개발한다고 가정해보죠. 그리고 아래의 기능이 동시에 수행되길 원한다고 해봅시다.

  1. 사용자로부터 입력받은 내용을 화면에 보여주는 기능
  2. 사용자가 입력한 내용이 맞춤법에 맞는지 검사하는 기능
  3. 사용자가 입력한 내용을 수시로 저장하는 기능

이 기능들을 작동시키는 코드를 각각의 스레드로 만들면 동시에 실행할 수 있습니다.

💡 스레드의 정의 정리

  • 하드웨어적 정의 : 하나의 코어가 동시에 처리하는 명령어 단위
  • 소프트웨어적 정의 : 하나의 프로그램에서 독립적으로 실행되는 단위

한 번에 하나씩 명령어를 처리하는 1코어 1스레드 CPU도 소프트웨어적 스레드를 수십 개 실행할 수 있습니다. 1코어 1스레드 CPU로도 프로그램의 여러 부분을 동시에 실행할 수 있죠

만약 스레드의 사전적 정의(실행 흐름의 단위)만을 암기한다면, **‘1코어 1스레드 CPU가 여러 스레드로 만들어진 프로그램을 실행할 수 있다’**라는 말이 어려울 겁니다. 해석해보면, 하드웨어적인 스레드가 1개여도 소프트웨어적인 스레드는 여러 개 만들 수 있습니다. 이런 이유로 하드웨어적 스레드와 소프트웨어적 스레드는 구분하여 기억하는 것이 좋습니다


1.3.3 멀티스레드 프로세서

이번 절에서 좀 더 자세히 학습할 건 하나의 코어로 여러 명령어를 동시에 처리하는 기술인 하드웨어적 스레드입니다.

  • 용어의 혼동을 방지하기 위해 이제부터
    • 소프트웨어적으로 정의된 스레드스레드,
    • CPU에서 사용되는 스레드하드웨어 스레드라고 지칭하겠습니다.

멀티스레드 프로세서하나의 코어로 여러 명령어를 동시에 처리하는 CPU라고 했었죠? 어떻게 이런 일이 가능할까요?

  • 멀티스레드 프로세서를 실제로 설계하는 일은 매우 복잡하지만, 가장 큰 핵심은 레지스터입니다.
  • 하나의 코어로 여러 명령어를 동시에 처리하도록 만들려면
    • 프로그램 카운터, 스택 포인터, 데이터 버퍼 레지스터, 데이터 주소 레지스터와 같이
    • 하나의 명령어를 처리하기 위해 꼭 필요한 레지스터를 여러 개 가지고 있으면 됩니다.
  • e.g. 프로그램 카운터가 2개 있다면, ‘메모리에서 가져올 명령어 주소’를 2개 지정할 수 있을 것이고,
  • e.g. 스택 포인터가 2개 있다면, 2개의 스택을 관리할 수 있겠죠?

MySelf-Architec-Oper_5_7

  • 하나의 명령어를 실행하기 위해 꼭 필요한 레지스터들을 편의상 레지스터 세트라고 표기했습니다.
  • 레지스터 세트가 1개인 CPU1개의 명령어를 처리하기 위한 정보들을 기억할 뿐이지만,
  • 레지스터 세트가 2개인 CPU2개의 명령어를 처리하기 위한 정보들을 기억할 수 있습니다.
  • 여기서 ALU와 제어장치2개의 레지스터 세트에 저장된 명령어를 해석하고 실행하면,
    • 하나의 코어에서 2개의 명령어가 동시에 실행됩니다.

MySelf-Architec-Oper_5_8

  • 하드웨어 스레드를 이용해 하나의 코어로도 여러 명령어를 동시에 처리할 수 있다고 했습니다.
  • 그러나 메모리 속 프로그램 입장에서 봤을 때,
    • 하드웨어 스레드는 마치 **‘한 번에 하나의 명령어를 처리하는 CPU’**나 다름없습니다.
  • e.g. 2코어 4스레드 CPU1번에 4개의 명령어를 처리할 수 있는데,
    • 프로그램 입장에서 봤을 땐, 1번에 하나의 명령어를 처리하는 CPU가 4개 있는 것처럼 보입니다.
    • 그래서 하드웨어 스레드논리 프로세서(logical processor)라고 부르기도 합니다

MySelf-Architec-Oper_5_9

이를 직접 확인해 볼까요? 필자는 4코어 8스레드 CPU를 사용하고 있습니다. 작업 관리자를 열어 [성능] 탭의 [CPU] 항목을 살펴보면, 논리 프로세서가 8임을 확인할 수 있습니다.

  • 실제 CPU 속에 명령어를 처리하는 부품(코어)은 4개지만,
  • 메모리 속 프로그램이 보기에는 1번에 하나의 명령어를 처리하는 부품이 마치 8개 있는 것처럼 보이기 때문에
  • 논리 프로세서가 8개로 나오는 것입니다.

이제 멀티코어와 멀티스레드의 차이를 알겠나요?

  • 코어는 **명령어를 실행할 수 있는 ‘하드웨어 부품’**이고,
  • 스레드는 **‘명령어를 실행하는 단위’**입니다.
  • 멀티코어 프로세서는 명령어를 실행할 수 있는 하드웨어 부품이 CPU 안에 2개 이상 있는 CPU를 의미하고,
  • 멀티스레드 프로세서하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU를 의미합니다.

2. 명령어 병렬 처리 기법

빠른 CPU를 만들려면, 높은 클럭 속도에 멀티코어, 멀티스레드를 지원하는 CPU를 만드는 것도 중요하지만, CPU가 놀지 않고 시간을 알뜰하게 쓰며 작동하게 만드는 것도 중요합니다.

MySelf-Architec-Oper_5_10

이번 절에서는 명령어를 동시에 처리하여 CPU를 한시도 쉬지 않고 작동시키는 기법인 명령어 병렬 처리 기법(ILP; Instruction-Level Parallelism)을 알아봅니다. 대표적인 명령어 병렬 처리 기법에는 명령어 파이프 라이닝, 슈퍼스칼라, 비순차적 명령어 처리가 있습니다.


2.1 명령어 파이프라인

명령어 파이프라인을 이해하려면, 하나의 명령어가 처리되는 전체 과정을 비슷한 시간 간격으로 나누어 보아야 합니다. 명령어 처리 과정클럭 단위로 나누어 보면, 일반적으로 다음과 같이 나눌 수 있 습니다

  1. 명령어 인출(Instruction Fetch)
  2. 명령어 해석(Instruction Decode)
  3. 명령어 실행(Execute Instruction)
  4. 결과 저장(Write Back)

💡물론 이 단계가 정답은 아닙니다.

전공서에 따라 명령어 인출 → 명령어 실행으로 나누기도 하고, 명령어 인출 → 명령어 해석 → 명령어 실행 → 메모리 접근 → 결과 저장으로 나누기도 합니다.

여기서 중요한 점은 같은 단계가 겹치지만 않는다면, **CPU는 ‘각 단계를 동시에 실행할 수 있다’**는 것입니다.

  • e.g. CPU는 한 명령어를 ‘인출’하는 동안에 **다른 명령어를 ‘실행’**할 수 있고,
    • 한 명령어가 ‘실행’되는 동안에 **연산 결과를 ‘저장’**할 수 있습니다.

MySelf-Architec-Oper_5_11

  • t1에는 명령어 1, 2를 동시에 처리할 수 있고,
  • t2에는 명령어 1, 2, 3을 동시에 처리할 수 있습니다.
  • 이처럼 명령어를 겹쳐서 수행하면, 명령어를 하나하나 실행하는 것보다 훨씬 더 효율적으로 처리할 수 있겠죠?

MySelf-Architec-Oper_5_12

이처럼 마치 공장 생산 라인(컨베이어 벨트)과 같이

  • 어떤 사람은 인출만 하고, 해석만하고, 실행만 하고, 저장만 하고를 쭉 반복합니다.
  • 명령어들명령어 파이프라인(instruction pipeline)에 넣고,
  • 동시에 처리 하는 기법명령어 파이프라이닝(instruction pipelining)이라고 합니다.

명령어 파이프라인을 사용하지 않았다면?

MySelf-Architec-Oper_5_13

  • 한눈에 봐도 명령어 파이프라이닝을 이용하는 것이 더 효율적임을 알 수 있습니다.
  • 모든 명령어를 순차적으로만 처리하면, 시간이 정말 길어지는 것을 볼 수 있습니다.

2.2 파이프라인 위험

  • 파이프라인 위험(pipeline hazard) : 특정 상황에서는 성능 향상에 실패하는 경우
  • 즉, 명령어들이 병렬로(겹쳐서) 제대로 실행되지 않는 경우
  • 파이프라인 위험에는 크게 3가지로 나뉩니다.
    • 데이터 위험(data hazard)
    • 제어 위험(control hazard)
    • 구조적 위험(structural hazard)

2.2.1 데이터 위험

  • 명령어 간 데이터 의존성에 의해 발생
  • 모든 명령어를 동시에 처리할 수는 없습니다.
    • 어떤 명령어는 이전 명령어를 끝까지 실행해야만 비로소 실행할 수 있는 경우가 있습니다.
  • e.g. 아래 명령어를 보면,
    • 편의상 레지스터 이름을 R1, R2, R3, R4, R5라 하고
    • 왼쪽 레지스터에 오른쪽 결과를 저장하라 기호로 표기
1
명령어 1: R1 ← R2 + R3 // R2 레지스터 값과 R3 레지스터 값을 더한 값을 R1 레지스터에 저장
2
명령어 2: R4 ← R1 + R5 // R1 레지스터 값과 R5 레지스터 값을 더한 값을 R4 레지스터에 저장
  • 위의 경우 명령어 1을 수행해야만 명령어 2를 수행할 수 있습니다.
    • 즉, R1에 R2 + R3 값이 저장되어야 명령어 2를 수행할 수 있습니다.
  • 만약 명령어 1 실행이 끝나기 전에 명령어 2를 인출하면,
    • R1에 R2 + R3 값이 저장되기 전에 R1 값을 읽어 들이므로, 원치 않은 R1 값으로 명령어 2를 수행합니다.
    • 따라서 명령어 2는 명령어 1의 데이터에 의존적입니다.
  • 이처럼 데이터 의존적인 두 명령어를 무작정 동시에 실행하려고 하면,
    • 파이프라인이 제대로 작동하지 않는 것데이터 위험이라고 합니다.

2.2.2 제어 위험

MySelf-Architec-Oper_5_14

  • 분기 등으로 인한 ‘프로그램 카운터의 갑작스러운 변화’에 의해 발생
  • 프로그램 실행 흐름이 바뀌어 명령어가 실행되면서, 프로그램 카운터 값에 갑작스러운 변화가 생긴다면,
    • 명령어 파이프라인에 미리 가지고 와서 처리 중이었던 명령어들은 아무 쓸모가 없어집니다.
    • 이를 제어 위험이라고 합니다.
    • e.g. jump, call 명령어, 인터럽트
  • e.g. 그림처럼 jump 60이란 명령어를 실행해보면,
    • 60번지로 실행의 흐름이 바뀌게 됩니다.
    • 프로그램 카운터도 60번지로 업데이트됩니다.
    • 그러나 명령어들은 기본적으로 명령어들을 순차적으로 겹쳐서 갖고 옵니다.
    • 프로그램 카운터가 갑작스럽게 특정 메모리 주소로 변경되버리면,
      • 즉, 다음부터 60번지 실행하라고 하면,
      • 그 다음에 겹쳐서 실행하고 있던 11번지, 12번지 명령어들은 아무 쓸모가 없어져 버립니다.
    • 이렇게 되면 파이프라이닝이 성능 향상에 실패하는 문제제어위험이라 합니다.
  • cf. 이러한 상황을 방지하기 위해서, 분기 예측(branch prediction)이라는 기술을 사용
    • 분기 예측 : 프로그램이 어디로 분기할지 미리 예측한 후 그 주소를 인출하는 기술

2.2.3 구조적 위험

  • 구조 위험 : 서로 다른 명령어가 동시에 ALU, 레지스터 등과 같은 CPU 부품을 사용하려고 할 때 발생
  • 구조적 위험은 자원 위험(resource hazard)이라고도 부릅니다.

2.3 슈퍼스칼라

파이프라이닝은 단일 파이프라인으로도 구현이 가능하지만, 오늘날 대부분의 CPU에서는 여러 개의 파이프라인을 이용합니다.

MySelf-Architec-Oper_5_15

  • 슈퍼 스칼라(superscalar) : CPU 내부에 여러 개의 명령어 파이프라인을 포함한 구조
    • 그림처럼 한 번에 여러 개의 명령어를 (겹쳐서) 인출, 해석하고, 실행하고 저장하는 것처럼 사용
    • e.g. 오늘날의 멀티스레드 프로세서
      • 한 번에 여러 명령어를 인출하고, 해석하고, 실행할 수 있기 때문에 슈퍼스칼라 구조를 사용

MySelf-Architec-Oper_5_16

  • e.g. 명령어 파이프라인을 하나만 두는 것이 마치 공장 생산 라인을 한 개 두는 것과 같다면,
    • 슈퍼스칼라공장 생산 라인을 여러 개 두는 것
  • 슈퍼스칼라 프로세서 또는 슈퍼스칼라 CPU
    • 슈퍼스칼라 구조로 명령어 처리가 가능한 CPU
  • 이론적으로는 파이프라인 개수에 비례하여 처리 속도 증가
  • But 파이프라인 위험도 증가로 인해 파이프라인 개수에 비례하여 처리 속도가 증가하진 않음
  • 이 때문에 슈퍼스칼라 방식을 차용한 CPU는 파이프라인 위험을 방지하기 위해 고도로 설계되어야 합니다.
  • 여러 개의 파이프라인을 이용하면
    • 하나의 파이프라인을 사용할 때보다
    • 데이터 위험, 제어 위험, 자원 위험을 피하기가 더욱 까다롭기 때문이죠.

2.4 비순차적 명령어 처리 : OoOE

  • 비순차적 명령어 처리(OoOE; Out-of-order execution)
    • 보통 OoOE로 줄여 부름
    • 이 기법은 많은 전공서에서 다루지 않지만,
    • 오늘날 CPU 성능 향상에 크게 기여한 기법이자 대부분의 CPU가 차용하는 기법
  • 이름에서도 알 수 있듯 명령어들을 순차적으로 실행하지 않는 기법
    • e.g. 명령어의 ‘합법적인 새치기’
  • 지금까지 설명했던 명령어 파이프라이닝, 슈퍼스칼라 기법은 모두 순차적으로 명령어를 처리하는 방식
    • But 파이프 라인 위험 같은 예상못한 문제들로 명령어는 곧바로 처리되지 못하기도 합니다.
    • 만약 모든 명령어를 순차적으로만 처리한다면,
    • 예상못한 상황에서 명령어 파이프라인은 멈춰 버리게 됩니다.
  • e.g. 아래 명령어를 보면,
    • 편의상 메모리 N번지M(N)으로,
    • **‘메모리 N번지에 M을 저장하라’**는 M(N) ← M으로 표기
1
M(100) ← 1
2
M(101) ← 2
3
M(102) ← M(100) + M(101)
4
M(150) ← 1
5
M(151) ← 2
6
M(152) ← 3
  • 위 명령어를 순차적으로만 처리한다면,
    • 3번 명령어를 실행하기 위해서는 M(100) 값은 물론 M(101) 값이 결정되어야 하기에
    • 1번과 2번 명령어 실행이 끝날 때까지 기다려야 합니다.

MySelf-Architec-Oper_5_17

  • 이 명령어들을 순차적으로 실행되는 CPU로 실행하면 위 그림과 같습니다.
  • 2번 명령어 실행이 끝날 때까지 3, 4, 5, 6 번 명령어들은 기다립니다.

그런데 여기서 의존성이 없는 명령어의 순서를 바꿔본다면? 가령 3번은 다음과 같이 뒤의 명령어와 순서를 바꾸어 실행해도 크게 문제될 것이 없습니다.

1
M(100) ← 1
2
M(101) ← 2
3
M(150) ← 1
4
M(151) ← 2
5
M(152) ← 3
6
M(102) ← M(100) + M(101)
  • 순차적으로 명령어를 처리할 때보다 더 효율적으로 처리됩니다.
  • 이렇게 명령어를 순차적으로만 실행하지 않고 순서를 바꿔 실행해도,
    • 무방한 명령어를 먼저 실행하여,
    • 명령어 파이프라인이 멈추는 것을 방지하는 기법비순차적 명령어 처리 기법이라고 합니다.

하지만 아무 명령어나 순서를 바꿔서 수행할 수는 없습니다.

1
M(100) ← 1
2
M(101) ← 2
3
M(102) ← M(100) + M(101)
4
M(103) ← M(102) + M(101)
5
M(104) ← M(100)
  • 3번 명령어와 1번 명령어의 순서를 바꿀 수는 없음
    • 3번 명령어 술행을 위해서는 반드시 M(100) 값이 결정되어야 함
  • 1번, 4번 명령어의 순서도 바꿀 수 없습니다.
    • 1번 명령어를 토대로 3번 명령어가 수행되고, 3번 명령어를 토대로 4번이 수행
  • 하지만 4번 명령어와 5번 명령어는 순서를 바꾸어 실행할 수 있습니다.
    • 다시 말해 두 명령어는 어떤 의존성도 없기에,
    • 순서를 바꿔도 전체 프로그램의 실행 흐름에는 영향이 없습니다.

이처럼 비순차적 명령어 처리가 가능한 CPU명령어들이 어떤 명령어와 데이터 의존성을 가지고 있는지, 순서를 바꿔 실행할 수 있는 명령어에는 어떤 것들이 있는지를 판단할 수 있어야 합니다.


3. CISC와 RISC

명령어 파이프라이닝슈퍼스칼라 기법은 현대 CPU가 꼭 사용해야 하는 핵심 기술입니다.

  • 그런데 명령어 파이프라이닝하기 유리하게 생긴 명령어가 있고,
    • 명령어 파이프라이닝하기 불리하게 생긴 명령어가 있습니다.
  • 그렇다면 어떻게 생겨야지 명령어 파이프라인에 유리할까요?
  • 이와 관련해 CPU의언어인 ISA와 각기 다른 성격의 ISA를 기반으로 설계된 CISC와 RISC를 알아보겠습니다.

3.1 명령어 집합

3장에서 CPU는 명령어를 실행한다고 했습니다. 그런데 한 가지 의문이 듭니다.

  • 이 세상 모든 CPU들이 똑같이 생긴 명령어를 실행할까요?
  • 세상에는 수많은 CPU 제조사들이 있고, CPU마다 규격과 기능, 만듦새가 다 다른데,
    • 모든 CPU가 이해하고 실행하는 명령어들이 다 똑같이 생겼을까요?
  • 명령어의 기본적인 구조와 작동 원리03장에서 학습한 내용에서 크게 벗어나지 않지만,
  • 명령어의 세세한 생김새, 명령어로 할 수 있는 연산, 주소 지정 방식 등은 CPU마다 조금씩 차이가 있습니다.
  • CPU가 이해할 수 있는 명령어들의 모음명령어 집합(instruction set)
    • 또는 명령어 집합 구조(ISA; Instruction Set Architecture)라고 합니다.
    • cf. 명령어 집합에 ‘구조’라는 단어가 붙은 이유는
      • CPU가 어떤 명령어를 이해하는지에 따라 컴퓨터 구조 및 설계 방식이 달라지기 때문
  • 즉, CPU마다 ISA가 다를 수 있다는 것입니다.

MySelf-Architec-Oper_5_18

예를 들어,

  • 인텔의 노트북 속 CPU는 x86 혹은 x86-64 ISA를 이해하고,
    • x86은 32비트용, x86-64는 64비트용 x86 ISA
  • 애플의 아이폰 속 CPU는 ARM ISA를 이해합니다
  • x86(x86-64)과 ARM은 다른 ISA이기 때문에
    • 인텔 CPU를 사용하는 컴퓨터와 아이폰은 서로의 명령어를 이해할 수 없습니다.
  • 실행 파일은 명령어로 이루어져 있고, 서로의 컴퓨터가 이해할 수 있는 명령어가 다르기 때문입니다

MySelf-Architec-Oper_5_19

또 소스코드 예를 보면,

  • 동일한 소스 코드를 작성하고, ISA가 다른 컴퓨터에서 어셈블리어로 컴파일하면 위와 같은 결과를 얻을 수 있습니다.
  • 왼쪽은 x86-64 ISA, 오른쪽은 ARM ISA입니다.
  • 똑같은 코드로 만든 프로그램임에도 CPU가 이해하고 실행할 수 있는 명령어가 달라
    • 어셈블리어도 다른 것을 알 수 있습니다.
  • cf. 사용한 컴파일러에 따라서도 어셈블리어가 달라질 수 있는데,
    • 위 예시에서는 gcc 11.2라는 동일한 컴파일러를 이용했습니다

ISA가 같은 CPU끼리는 서로의 명령어를 이해할 수 있지만, ISA가 다르면 서로의 명령어를 이해하지 못합니다. 이런 점에서 볼 때, ISA는 일종의 CPU의 언어인 셈입니다.

MySelf-Architec-Oper_5_20

각기 다른 언어를 사용하는 나라들을 보면,

  • 사용하는 언어만 다른 게 아니라 언어에 따라 사람들의 가치관과 생활 양식도 다른 것을 볼 수 있습니다.
  • 마치 높임말이 있는 나라에서는 비교적 어른을 공경 하는 문화가 자리잡혀 있고,
  • 높임말이 없는 나라에서는 비교적 평등한 문화가 자리잡힌 것처럼요.

MySelf-Architec-Oper_5_21

CPU도 마찬가지입니다. CPU가 이해하는 명령어들이 달라지면 비단 명령어의 생김새만 달라지는 게 아닙니다.

  • CPU를 비롯한 하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속이라고도 볼 수 있습니다.
  • 명령어 집합(구조) : CPU의 언어인 셈
  • 명령어가 달라지면, 그에 대한 나비효과로 많은 것들이 달라진다.
    • 제어장치가 명령어를 해석하는 방식, 사용되는 레지스터의 종류와 개수, 메모리 관리 방법 등
  • 그리고 이는 곧 CPU 하드웨어 설계에도 큰 영향을 미칩니다.

앞서 명령어 병렬 처리 기법들을 학습했습니다. 이를 적용하기에 용이한 ISA가 있고, 그렇지 못한 ISA도 있습니다. 다시 말해 명령어 파이프라인, 슈퍼스칼라, 비순차적 명령어 처리를 사용하기에 유리한 명령어 집합이 있고, 그렇지 못한 명령어 집합도 있습니다. 그렇다면 명령어 병렬 처리 기법들을 도입하기 유리한 ISA는 무엇일까요? 이와 관련해 현대 ISA의 양대 산맥인 CISCRISC에 대해 알아보겠습니다.


3.2 CISC

  • CISC(Complex Instruction Set Computer, 시스크)
    • 이를 그대로 해석하면 **‘복잡한 명령어 집합을 활용하는 컴퓨터’**를 의미
    • 여기서 ‘컴퓨터’를 ‘CPU’를 의미
  • 말 그대로 그대로 복잡하고 다양한 명령어들을 활용하는 CPU 설계 방식
    • e.g. inter이나 amd 사의 대표되는 x86, x86-64는 대표적인 CISC 기반의 ISA

MySelf-Architec-Oper_5_22

  • CISC는 복잡한 명령어 집합을 사용하기 때문에, 복잡하고 다양한 명령어들을 사용합니다.
  • 명령어의 크기과 형태가 다양하고, 이를 가변길이 명령어라고도 합니다.
  • e.g. 위 그림처럼 어떤 명령어는 독특한 기능을 수행하는 것도 있고
    • 또 다른 명령어는 또 다른 독특한 주소 지정 기능을 지원하기도 합니다.

MySelf-Architec-Oper_5_23

  • **‘다양하고 강력한 명령어를 활용한다‘**는 말은
    • 상대적으로 적은 수의 명령어로도 프로그램을 실행할 수 있다는 것을 의미합니다.

MySelf-Architec-Oper_5_19

예를 들어, 어셈블리어 코드를 다시 보면,

  • x86-64 코드 길이가 ARM보다 짧죠?
  • 이는 ARM 명령어 여러 개로 수행할 수 있는 일을 x86-64 명령어 몇 개만으로도 수행할 수 있음을 알 수 있습니다.
  • **‘프로그램을 실행하는 명령어 수가 적다‘**는 말은 **‘컴파일된 프로그램의 크기가 작다’**는 것을 의미합니다.
    • 같은 소스 코드를 컴파일해도, CPU마다 생성되는 실행 파일의 크기가 다를 수 있다는 것이죠.

이런 장점 덕분에 CISC메모리를 최대한 아끼며 개발해야 했던 시절에 인기가 높았습니다.

  • But **‘명령어 파이프라이니이 불리하다‘**는 치명적인 단점
  • 명령어가 워낙 복잡하고 다양한 기능을 제공하는 탓에
    • 명령어의 크기와 실행되기까지의 시간이 일정하지 않음
  • 복잡한 명령어 때문에, 명령어 하나를 실행하는데에 여러 클럭 주기가 필요
  • 이는 명령어 파이프라인을 구현하는 데에 큰 걸림돌이 됩니다.
  • e.g.

MySelf-Architec-Oper_5_24

  • 명령어 파이프라인 기법을 위한 이상적인 명령어
    • 위 그림과 같이 각 단계에 소요되는 시간이 (가급적 1 클럭으로) 동일해야 합니다.
    • 그래야 파이프라인이 마치 공장의 생산 라인처럼 착착착 결과를 낼 테니까요

MySelf-Architec-Oper_5_26

  • 하지만 CISC가 활용하는 명령어명령어 수행 시간이 길고 가지각색이기 때문에
    • 파이프라인효율적으로 명령어를 처리할 수 없습니다.
    • 즉, 규격화되지 않은 명령어가 파이프라이닝을 어렵게 만든 셈이죠.
  • 명령어 파이프라인이 제대로 동작하지 않는다는 것은 현대 CPU에서 아주 치명적인 약점입니다.
  • 현대 CPU에서 명령어 파이프라인높은 성능을 내기 위해 절대 놓쳐서는 안되는 핵심 기술이기 때문입니다.

MySelf-Architec-Oper_5_27

게다가 CISC가 복잡하고 다양한 명령어를 활용가능하다고 하지만, 대다수의 복잡한 명령어는 사용빈도가 낮습니다.

  • 1974년 IBM 연구소의 존 코크(John Cocke)
    • CISC 명령어 집합 중 불과 20% 정도의 명령어가 사용된 전체 명령어의 80%가량을 차지한다는 것을 증명
  • 즉, CISC 명령어 집합이 다양하고 복잡한 기능을 지원하지만, 실제로는 자주 사용되는 명령어만 쓰였다는 것

💡 CISC 명령어 집합 정리

  • 복잡하고 다양한 기능을 제공해서, 적은 수의 명령으로 프로그램을 동작시키고 메모리를 절약할 수 있지만,
  • 다음과 같은 이유로 CISC 기반 CPU는 성장에 한계가 있습니다.
    • 명령어의 규격화가 어려워 파이프라이닝이 어렵습니다
    • 거기다 대다수의 복잡한 명령어는 그 사용 빈도가 낮습니다.

3.3 RISC

💡 CISC 기반 CPU 한계 극복 방안

  • 명령어의 규격화가 어려워 파이프라이닝이 어렵습니다
    • → 원활한 파이프라이닝을 위해 ‘명령어 길이와 수행 시간이 짧고 규격화’
  • 대다수의 복잡한 명령어는 그 사용 빈도가 낮습니다.
    • → 복잡한 기능을 지원하는 명령어를 추가하기보다는
    • → **‘자주 쓰이는 기본적인 명령어를 작고 빠르게 만드는 것’**이 중요

CISC의 한계를 해결하기 위해 등장한 것이 최근 각광받고 있는 것이 RISC입니다.

MySelf-Architec-Oper_5_28

  • RISC(Reduced Instruction Set Computer, 리스크)
  • 이름처럼 명령어의 종류가 적고, 짧고 규격화된 명령어를 사용합니다.
    • 되도록 1클럭 내외로 실행되는 명령어를 지향합니다.
    • 즉, RISC는 고정 길이 명령어를 활용합니다.

MySelf-Architec-Oper_5_29

  • RISC는 SISC에 비해 비교적 짧고 규격화된 명령어들을 활용하기 때문에
    • 명령어 파이프라이닝에 최적화되어 있습니다.
  • 메모리에 직접 접근하는 명령어를 load, store로 제한할 만큼 메모리 접근을 단순화하고 최소화를 추구
    • e.g. 메모리에 접근하는 것을 load
    • e.g. 메모리에 저장하는 것을 store
    • cf. 그렇기 때문에 CISC보다 주소 지정 방식의 종류가 적은 경우가 많습니다
    • cf. 이런 점에서 RISC를 load-store 구조라고 부르기도 함
  • 메모리 접근을 단순화, 최소화하는 대신 레지스터를 적극적으로 활용합니다.
    • 그렇기에 CISC보다 레지스터를 이용하는 연산이 많고,
    • 일반적인 경우보다 범용 레지스터 개수도 더 많습니다.
    • 다만 사용 가능한 명령어 개수가 CISC보다 적기 때문에
    • RISC는 CISC보다 많은 명령으로 프로그램을 작동시킵니다.

MySelf-Architec-Oper_5_19

  • 어셈블리어 예시를 다시 볼까요?
    • ARMRISC 기반의 대표적인 ISA입니다.
    • x86-64보다 더 많은 명령어로 동일한 프로그램을 실행하는 것을 볼 수 있습니다.
    • 즉, 같은 소스 코드를 컴파일해도 RISC는 CISC보다 많은 수의 명령어로 변환됩니다.

마지막으로 CISC와 RISC의 차이를 정리하고 마무리하겠습니다.

CISCRISC
복잡하고 다양한 명령어단순하고 적은 명령어
가변 길이 명령어고정 길이 명령어
다양한 주소 지정 방식적은 주소 지정 방식
프로그램을 이루는 명령어의 수가 적음프로그램을 이루는 명령어의 수가 많음
여러 클럭에 걸쳐 명령어 수행1클럭 내외로 명령어 수행
파이프라이닝하기 어려움파이프라이닝하기 쉬움