본문 바로가기
AI & CS 지식/컴퓨터구조

[혼공컴운] 5. CPU 성능 향상 기법

by rahites 2024. 8. 15.

CPU 성능 향상 기법

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

한창 조립 컴퓨터를 맞추기 위해 쿨앤조이, 퀘이사존같은 커뮤니티를 둘러보다 보면 클럭, 코어 등 알지 못하는 용어를 접하는 경우가 있었다. 요즘은 대다수의 PC방 들에서 사양 좋은 컴퓨터를 홍보하기 위해 본인들이 가지고 있는 컴퓨터의 스펙을 길거리에 붙여놓는 등 좋은 컴퓨터임을 나타낼 수 있는 단어들이 더욱 친근하게 다가오고 있다.

 

이번 장에서는 컴퓨터의 성능이 좋다는 것을 간접적으로 알려주는 용어들에 대해 알아보며 그 중에서 특히 클럭, 코어, 스레드를 집중적으로 분석한다.

 

클럭

클럭은 4장에서 살펴본 바와 같이 컴퓨터 부품 속 명령어 사이클이 수행되는 단위이다. 예를 들어 클럭 신호가 빠르게 반복되면(클럭 속도가 높아지면) CPU를 비롯한 컴퓨터 부품들은 빠르게 움직인다.

 

클럭 속도는 Hz 단위로 측정하며 이는 1초에 클럭이 몇번 반복되는지를 나타낸다. 예를 들어 100Hz의 경우 하나의 클럭이 수행되는데 걸리는 시간은 0.01초이다.

AMD Ryzen 5 9600X

위는 며칠 전 공개된 AMD Ryzen 5 9600X CPU의 성능이다. 최신 CPU 모델은 거의 4GHz 수준의 성능을 보이는 것을 확인할 수 있다.

 

하지만 클럭 속도는 일정하지 않다. 예를 들어 위 사진 속 CPU에는 기본 클럭 속도와 최대 부스트 클럭 속도가 모두 기재되어 있는데 평균적으로는 3.9GHz 정도의 성능을 내지만 고성능을 요하는 때에는 순간적으로 5.4GHz 수준의 성능을 낼 수도 있다.

 

최대 클럭 속도를 강제로 더 끌어올리는 기법을 오버클럭킹(Overclocking)이라고 하며 이 경우 발열이 많아지는 문제가 발생할 수 있다.

 

코어와 멀티코어

CPU의 성능을 올리는데에는 클럭 속도를 올리는 것 말고 다른 방법또한 존재한다. 대표적인 방법은 코어와 스레드 수를 늘리는 것이다. 

 

CPU의 기능을 다시 한번 생각해보자. CPU는 명령어를 실행하는 부품이다. 하지만, 이 부품이 많아진다면 어떨까? 연산 처리 성능이 더욱 높아지지 않을까?

 

그렇게 등장한 것이 코어의 개념이다. 코어는 이전까지 알고있던 CPU의 기능을 완벽히 수행하며 기술이 발전하며 하나의 CPU안에 여러개의 코어가 들어갈 수 있었다. 사람으로 따지면 하나의 CPU 안에 여러개의 뇌가 들어간 것이다. 이렇게 하나의 CPU 안에 여러개의 코어를 가지는 CPU를 멀티코어 CPU(멀티코어 프로세서)라고 한다.

클럭 속도가 조금 더 높더라도 코어 수가 더 늘어나면 성능이 더 높아지게 된다. (물론 N개가 들어난다고 N배로 좋아지지는 않음)

 

스레드와 멀티스레드

스레드(Thread)의 사전적 의미는 실행 흐름의 단위이다. 스레드는 하드웨어적 스레드와 소프트웨어적 스레드로 나뉜다.

 

1) 하드웨어적 스레드

하드웨어적 스레드는 하나의 코어가 동시에 처리하는 명령어 단위를 의미한다. 즉, 하나의 코어 안에서 존재하는 스레드의 수만큼 명령어를 동시에 처리할 수 있다는 것이다. 예를들어 위에서 예시로 든 Ryzen 5 9600X 모델의 경우 6코어 12스레드이기 때문에 6개의 코어가 각각 2개의 하드웨어 스레드를 처리한다고 볼 수 있다.

 

SMT(Simultaneous Multi Threading) 기술로도 부르며 인텔은 해당 기술에 대해 하이퍼스레딩이라고 자체적으로 명명하였다.

 

2) 소프트웨어적 스레드

소프트웨어적 스레드는 하나의 프로그램에서 독립적으로 실행되는 단위를 의미한다. 우리가 일반적으로 코딩을 할 때 사용하는 스레드의 개념은 이 소프트웨어적 스레드를 의미한다.

 

프로그램이 수행되어야 하는 여러 기능이 멀티 스레드를 통해 동시에 수행될 수 있으며, 앞서 알아본 하드웨어적 스레드와의 관계를 살펴보면 

하나의 코어에 있는, 1스레드 CPU도 여러개의 소프트웨어적 스레드를 실행할 수 있다.

 

위와 같은 내용 때문에 하드웨어적 스레드와 소프트웨어적 스레드는 분리하여 개념을 익혀두는 것이 좋다.

 

멀티스레드 프로세서

앞서 살펴보았듯이 멀티스레드 프로세서는 하나의 코어로 여러 명령어를 동시에 처리하는 CPU를 의미한다.

 

멀티스레드 프로세서를 구현하는 가장 큰 핵심은 레지스터이다. 이전 장에서 배운 프로그램 카운터, 스택 포인터 등 꼭 필요한 레지스터를 N개 가지고 있다면 N개의 명령어를 저장-해석-실행 할 수 있다. 프로그램 입장에서는 명령어를 처리하는 CPU가 N개가 있는 것처럼 보이기 때문에 하드웨어 스레드를 논리 프로세서(Logical Processor)라고 부르기도 한다.

 

2. 명령어 병렬 처리 기법

하드웨어적으로는 멀티코어, 멀티스레드를 지원하는 것이 CPU 속도에 중요했다. 하지만 하드웨어만 준비되었다고 CPU가 준비한 공간을 모두 사용할 수 있는 것은 아니다. 따라서 이번 장에서는 물리적으로 준비된 CPU 공간을 최대한 효율적으로 사용하는 명령어 병렬처리 기법(ILP; Instruction-Level Parallelism)에 대해 알아보도록 하겠다.

 

대표적인 명령어 병렬 처리 기법에는 명령어 파이프라이닝, 슈퍼스칼라, 비순차적 명령어 처리가 있다.

 

명령어 파이프라인

명령어 처리 과정을 클럭 단위로 나누어보면 아래와 같이 나눌 수 있다(나누는 방법은 여러가지가 있으며 그 중 하나의 방법).

  1. 명령어 인출
  2. 명령어 해석
  3. 명령어 실행
  4. 결과 저장

위처럼 4개의 단계로 나누었을 때, CPU는 같은 단계가 겹치지 않는다면 각 단계를 동시에 실행할 수 있다. 따라서 명령어를 겹쳐서 수행한다면 명령어를 하나씩 실행하는 것보다 효율적이다.

 예를 들어 현재 명령어 1에 대해 인출 단계를 수행하고 있다면 명령어 2에 대한 해석 단계, 명령어 3에 대한 실행 단계를 동시에 수행할 수 있다는 의미이다. 

이와 같은 방식을 마치 공장 생산 라인과 같다하여 명령어 파이프라이닝 기법이라고 한다(명령어를 명령어 파이프라인에 넣고 처리).

 

물론 파이프라이닝을 하였을 때의 성능이 효율적이지만 특정 상황에서는 성능이 높아지지 않는다. 이러한 상황을 파이프라인 위험(Pipeline Hazard)라고 한다. 파이프라인 위험에는 데이터 위험, 제어 위험, 구조적 위험이 존재한다.

  • 데이터 위험(Data Hazard)
    데이터 위험은 명령어 간 데이터 의존성에 의해 발생한다. 명령어들 중에서는 동시에 처리할 수 없고 이전 명령어를 실행해야만 다음 명령어를 실행할 수 있는 경우가 존재한다. 따라서 이렇게 의존적인 명령어들을 동시에 실행하려 하면 파이프라인이 제대로 작동하지 않는다.
  • 제어 위험(Control Hazard)
    제어 위험은 프로그램 카운터의 갑작스러운 변화에 의해 발생한다. 기본적으로 프로그램 카운터는 현재 실행 중인 명령어의 다음 주소로 갱신되는데, 프로그램 실행 흐름이 바뀌어 프로그램 카운터의 값이 갑작스럽게 바뀐다면 기존 명령어 파이프라인에 가져온 명령어들은 필요가 없어지고 파이프라인이 제대로 작동하지 않는다. 이러한 제어 위험을 방지하기 위해(갑작스러운 분기를 막기 위해) 프로그램이 어디로 분기할지 미리 예측하는 분기 예측 기술이 존재한다.
  • 구조적 위험(Structural Hazard)
    구조적 위험은 명령어들을 겹쳐 실행하는 과정에서 서로 다른 명령어가 동시에 같은 CPU 부품을 사용하려 할 때 발생한다. 구조적 위험은 자원 위험(Resource Hazard)라고도 부른다.

정리해보자면 파이프라이닝에 문제가 생기는 원인은 명령어간에 의존성이 존재하는 경우, 프로그램이 갑작스럽게 다른 주소로 분기하는 경우, 명령어들이 같은 CPU 자원을 사용하려 하는 경우가 있다.

 

슈퍼스칼라

슈퍼스칼라는 앞서 설명한 파이프라인을 CPU 내부에 여러개 포함하고 있는 구조를 의미한다.

 

명령어를 겹쳐 실행할 수 있는 파이프라인을 병렬로 두고 있는 것으로 생각할 수 있으며, 슈퍼스칼라 구조로 명령어 처리가 가능한 CPU를 슈퍼스칼라 프로세서(슈퍼스칼라 CPU)라고 부른다. 이론적으로는 파이프라인 개수에 비례하여 프로그램 처리 속도가 빨라져야 하지만, 항상 비례하지는 않는다.

 

여러 개의 파이프라인을 사용하기 때문에 파이프라이닝에 문제가 생기는 위험들을 방지하기 위해 고도로 설계되어야 한다.

 

비순차적 명령어 처리(OoOE; Out-of-order execution)

비순차적 명령어 처리는 명령어들을 순차적으로 실행하지 않는 방식이다. 이전 파이프라이닝은 명령어가 순차적으로 실행될 것을 가정하고 겹쳐서 실행했었지만, 이 방법에서는 특정 명령어들의 합법적인 새치기를 허용한다.

 

명령어가 순차적으로 실행되지 않더라도 괜찮은 명령어를 먼저 실행하여 명령어 파이프라인의 쓸데없는 딜레이를 방지한다.

예를 들어 (각 명령어들은 각자 다른 파이프라인에서 처리된다고 생각)
1. a=1

2. b=2
3. c=a+b
4. d=4
5. e=5
명령어들이 위와 같은 순서를 가지고 있을 때, 4, 5번 명령어의 수행은 3번과 의존성이 없지만 3번 명령어가 1, 2번 명령어의 처리를 기다리는 동안 같이 기다려야 한다. 이러한 비효율성을 아래와 같이 의존성이 없는 데이터의 순서를 바꾸어 처리하는 방식이 비순차적 명령어 처리 방식이다.
1. a=1
2. b=2
3. d=4
4. e=5
5. c=a+b

물론 데이터 의존성이 존재하는 명령어의 순서는 바꿀 수 없다.

 

3. CISC와 RISC

명령어 파이프라이닝과 슈퍼스칼라 기법을 실제로 CPU에 적용하기 위해서는 명령어가 파이프라이닝에 최적화되어 있어야 한다. 그러기 위해서는 명령어 자체가 파이프라이닝하기 쉽도록 구성되어 있어야 한다.

 

그렇다면 파이프라이닝하기 쉬운 명령어란 무엇일까?

 

이를 알기 위해 CPU 언어인 ISA와 ISA를 기반으로 설계된 CISC, RISC를 학습해 보겠다.

 

명령어 집합

인텔, AMD 등 CPU를 만드는 회사는 다양하고 한 회사라 하더라도 여러 세대의 CPU를 만든다. 이 CPU들은 각각 규격, 기능이 모두 다르며 그렇기 때문에 CPU가 이해하고 실행하는 명령어의 형태도 가지각색이다. 

 

이 때 CPU가 이해할 수 있는 명령어의 모음을 명령어 집합(Instruction set) 또는 명령어 집합 구조(ISA; Instruction Set Architecture)라고 한다. 즉 CPU마다 가지는 ISA가 다를 수 있으며 명령어가 달라지므로 이를 읽기 편하게 표현한 어셈블리어도 CPU마다 달라진다.

ISA가 다른 CPU끼리는 서로의 명령어를 이해할 수 없다.

똑같은 코드로 만든 프로그램일지라도 CPU가 이해하고 실행할 수 있는 명령어가 다르기 때문에 어셈블리어가 달라진다. 

 

ISA는 CPU의 언어이자 하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속이다. 그렇기 때문에 ISA가 달라지면 제어장치가 명령어를 해석하는 방식, 사용되는 레지스터의 종류와 개수, 메모리 관리 방법 등 다양한 것들이 달라지게 된다.

 

예를 들어 위에서 공부한 명령어 병렬 처리 기법을 도입하기에 유리한 ISA가 있고 그렇지 않은 ISA가 존재한다. 이를 설명하기 위해 최근 인기있는 ISA인 CISC와 RISC에 대해 공부해보자.

 

CISC

CISC란 Complex Instruction Set Computer의 약자로 한국어로 해석하면 "복잡한 명령어 집합을 활용하는 컴퓨터"를 의미한다. 말 그대로 다양하고 강력한 기능의 명령어 집합을 활용하기 때문에 명령어의 형태와 크기가 다양한 가변 길이 명령어를 사용한다. 이 때문에 메모리에 접근하는 주소 지정 방식도 다양해서 CISC만의 독특한 주소 지정 방식들도 존재한다.

ex. x86, x86-64

 

다양하고 강력한 명령어 집합을 사용하는 덕에 적은 수의 명령어로도 프로그램을 실행하는 것이 가능하다. 이러한 장점 덕분에 CISC는 메모리를 최대한 아끼며 개발하는 상황에서 장점을 가진다.

실제로 같은 소스 코드를 CISC 기반인 x86-64와 RISC 기반인 ARM 어셈블리어로 컴파일을 진행하였을 때 CISC 기반인 x86-64d의 코드 길이가 ARM보다 짧다. 이는 컴파일된 프로그램의 크기가 작다는 것을 의미하여 더 적은 실행 파일의 크기를 가진다고 볼 수 있다.

 

하지만, CISC 방법에는 몇가지 단점이 존재한다.

 

우선, CISC가 복잡한 명령어 집합을 활용하기 때문에 명령어의 크기나 실행되기까지의 시간이 일정하지 않다. 또한 복잡한 명령어 때문에 명령어 하나를 실행하는데에 여러 클럭 주기를 필요로 하기도 한다. 이러한 점들은 CISC에서 명령어 파이프라인을 구현하는데에 문제를 일으킨다.

 

명령어 파이프라인을 구축할 때에는 명령어가 수행하는 인출/해석/실행/저장 단계의 소요시간이 동일한 것이 가장 이상적이다. 하지만 CISC가 활용하는 명령어는 명령어 수행 시간이 길고 일정하지 않아 파이프라인이 효율적으로 명령어를 처리할 수 없다(각 단계가 걸리는 시간이 다 다름).

 

또한, 대다수의 복잡한 명령어들의 사용 빈도가 낮다. CISC 명령어 집합이 다양하고 복잡한 기능을 지원하지만 실제로는 자주 사용되는 명령어만 사용된다는 의미이다(자주 쓰는 명령어만 계속 쓰게 된다면 복잡한 명령어를 잘 처리할 수 있는 능력의 장점이 떨어진다!). 이러한 단점들로 인해 CISC 기반의 CPU는 그 성장에 있어 한계점이 존재한다.

 

RISC

CISC에서 얻은 한계점에 대해 극복해야 하는 것들은 다음과 같다.

  • 효율적인 파이프라이닝 구축을 위하여 명령어 길이와 수행 시간이 짧고 규격화 되어야 한다.
  • 어차피 자주 쓰이는 명령어들만 많이 사용되기에 복잡한 기능을 지원하는 명령어를 추가하기보다 자주 쓰이는 기본적인 명령어를 작고 빠르게 만드는 것이 중요하다.

위와 같은 한계점들을 극복하기 위해 등장한 것이 RISC(Reduced Instruction Set Computer)이다. RISC는 CISC에 비해 명령어의 종류가 적고 짧고 규격화된 명령어를 지향한다. 즉, RISC는 고정 길이 명령어를 사용한다.

 

명령어가 규격화 되어 있고, 비교적 1클럭 내외로 실행되기 때문에 RISC 명령어 집합은 명령어 파이프라이닝에 최적화되어 있다. 때문에 CISC 대비 주소 지정 방식의 종류가 적은 경우가 많다.

이를 위해 RISC는 메모리에 직접 접근하는 명령어를 load, store 2개로 제한하였으며 이러한 이유로 RISC를 load-store 구조라고 부르기도 한다.

 

RISC는 메모리 접근을 최소화하는 대신 레지스터를 적극 활용한다. 그렇기 때문에 CISC보다 레지스터를 이용하는 연산이 많고 범용 레지스터 개수도 더 많은 편에 속한다.

 

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

 

댓글