CPU 성능 향상 기법

2024. 5. 16. 15:33CS/컴퓨터구조

멀티코어 프로세서와 멀티스레드 프로세서

빠른 CPU를 위한 설계 기법에는 어떤 것이 있을까?

 

먼저 클럭 속도를 높이는 방법이 있다. 클럭 신호를 높이면 CPU를 비롯한 컴퓨터 부품들이 빠른 박자에 맞춰 움직이고 명령어 사이클이 더 빠르게 반복된다. 하지만 무작정 클럭 속도를 높이면 발열 문제가 심각해지고 CPU 성능 향상에 한계가 있다.

 

클럭 속도를 높이는 방법 말고 대표적인 해결책으로 코어를 늘리는 방법이 있다.

우선 과거 CPU는 명령어를 실행하는 부품으로 원칙적으로 하나만 존재했다.

하지만 오늘날의 CPU는 명령어를 실행하는 부품을 여러 개 포함하는 부품으로 명칭의 범위가 확장되었다.

코어를 여러 개 포함하고 있는 CPU를 멀티코어 CPU, 멀티코어 프로세서라고 부른다.

하지만 CPU의 연산 속도는 꼭 코어 수에 비례하여 증가하지는 않는다. 또한 작업량보다 코어 수가 지나치게 많은 것도 성능에는 큰 영향이 없다. 중요한 것은 코어마다 처리할 명령어를 적절하게 분배하는 것이다. 팀 프로젝트를 생각하면 이해하기 쉽다.

멀티코어의 예시

 

코어의 수를 늘리는 것 말고 스레드의 수를 늘리는 것도 CPU 성능 향상에 도움이 된다.

스레드는 실행 흐름의 단위를 의미한다. 우선 구분을 하자면 CPU에서 사용되는 하드웨어적 스레드가 있고, 프로그램에서 사용되는 소프트웨어적 스레드가 있다.


🤔하드웨어적 스레드와 소프트웨어적 스레드

하드웨어적 스레드는 하나의 코어가 동시에 처리하는 명령어의 단위를 의미한다.
예를 들어 2코어 4스레드 CPU는 명령어를 실행하는 부품 두 개를 포함하고, 한 번에 네 개의 명령어를 처리할 수 있는 CPU이다. 이처럼 하나의 코어로 여러 명령어를 동시에 처리하는 CPU를 멀티스레드 프로세서, 멀티스레드 CPU라고 한다.


소프트웨어적 스레드는 운영체제에서 언급하는 스레드의 개념이다. 즉, 하나의 프로그램에서 독립적으로 실행되는 단위를 의미한다. 예를 들어 워드 프로세스를 사용할 때 우리가 입력하는 내용을 화면으로 보여주는 동시에 맞춤법 검사도 하고, 자동으로 저장도 한다. 이 각각의 단위들이 스레드로 하나의 프로그램에서 독립적으로 실행되는 것이다.

따라서 "1코어 1스레드 CPU가 여러 스레드로 만들어진 프로그램을 실행할 수 있다." 라는 말은 맞는 말이다.
여기서 여러 스레드는 소프트웨어적 스레드를 의미하는 것이다.


 

그렇다면 하나의 코어로 여러 명령어를 처리하는 멀티스레드 프로세서는 어떻게 가능한 것일까?

핵심은 레지스터이다. 즉, 하나의 명령어를 처리하기 위해 꼭 필요한 레지스터를 여러 개 가지고 있으면 된다.

 

2코어 4스레드는 한 번에 네 개의 명령어를 처리할 수 있는데, 프로그램 입장에서는 한 번에 하나의 명령어를 처리하는 CPU가 4개 있는 것처럼 보인다. 그래서 하드웨어 스레드를 논리 프로세서라고 부른다.

아래 그림에서 논리 프로세서는 4이다. 실제 명령어를 처리하는 부품(코어)은 2개이지만 프로그램 입장에서는 한 번에 하나의 명령어를 처리하는 부품이 4개 있는 것처럼 보인다는 뜻이다.

정리하자면 다음과 같다.

  • 코어 : 명령어를 실행할 수 있는 하드웨어 부품
  • 스레드 : 명령어를 실행하는 단위
  • 멀티코어 프로세서 : 코어가 CPU 안에 두 개 이상 있는 CPU
  • 멀티스레드 프로세서 : 하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU

명령어 병렬 처리 기법

CPU의 성능을 높히려면 CPU가 명령어를 동시에 처리하면서 쉬지 않고 작동시키는 명령어 병렬 처리 기법이 사용되야한다. 대표적인 명령어 병렬 처리 기법에는 명령어 파이프라이닝, 슈퍼스칼라, 비순차적 명령어 처리가 있다.

 

명령어 파이프라이닝은 명령어들을 명령어 파이프라인에 넣고 동시에 처리하는 기법을 의미한다.

하나의 명령어가 처리되기 위해서는 다음과 같은 과정을 거친다. 이는 전공서마다 다르게 표현하기도 한다.

 

명령어 인출 ➡ 명령어 해석 ➡ 명령어 실행 ➡ 결과 저장

 

중요한 것은 같은 단계가 겹치지만 않는다면 CPU는 아래 그림처럼 각 단계를 동시에 실행할 수 있다.

현재 t3 지점에서 명령어1을 실행하면서 명령어2는 해석하고 있으며 명령어3은 인출하고 있다.

명령어 파이프라이닝

 

다만 파이프라인 위험이 존재한다. 이는 특정 상황에서는 성능 향상에 실패하는 경우를 의미한다.

파이프라인 위험에는 데이터 위험, 제어 위험, 구조적 위험이 있다.

 

데이터 위험은 데이터 의존성에 의해 발생한다.

예를 들어 다음과 같이 명령어가 있다고 생각해보자.

 

명령어1 : R1 ⬅ R2 + R3 // R2의 값과 R3의 값을 더해서 R1에 저장

명령어2 : R4 ⬅ R1 + R5 // R1의 값과 R5의 값을 더해서 R4에 저장

 

위의 경우에는 명령어1이 수행되어야 명령어2가 올바르게 수행될 수 있다. 만약 명령어1의 수행이 끝나기 전에 명령어2를 인출한다면 R1에 R2와 R3를 더한 값이 저장되기 전에 R1 값을 읽어들이므로 원치 않은 R1의 값으로 명령어2를 수행하게된다.

 

제어 위험은 주로 분기 등으로 인한 프로그램 카운터의 갑작스러운 변화에 의해 발생한다.

프로그램 카운터는 현재 실행 중인 명령어의 다음 주소로 갱신되는데 명령어를 실행하는 도중 프로그램 실행 흐름이 바뀌어 프로그램 카운터의 값에 변화가 생긴다면 파이프라인에 미리 가져온 명령어들이 쓸모가 없어지게 된다.

 

구조적 위험은 명령어들을 겹쳐 실행하는 과정에서 서로 다른 명령어가 동시에 ALU, 레지스터 등과 같은 CPU 부품을 사용하려고 할 때 발생하는 자원 위험이다.


 

슈퍼스칼라는 CPU 내부에 여러 개의 명령어 파이프라인을 포함한 구조를 의미한다.

그리고 슈퍼스칼라로 명령어 처리가 가능한 CPU를 슈퍼스칼라 프로세서, 슈퍼스칼라 CPU라고 한다.

그림에서 알 수 있듯이 슈퍼스칼라 프로세서는 동시에 여러 명령어를 인출할 수 있고 실행할 수도 있다.

이론적으로는 파이프라인의 갯수에 비례하여 처리 속도가 빨라지지만 파이프라인 위험 등의 문제로 인해 반드시 파이프라인 갯수에 처리속도가 비례하여 빨라지진 않는다. 


 

비순차적 명령어처리는 명령어의 합법적인 새치기이다.

예를 들어 아래와 같은 명령어들로 이루어진 소스코드가 있다고 생각하자.

  1. M(100) ⬅ 1 // 메모리 100번지에 1을 저장하라
  2. M(101) ⬅ 2 // 메모리 101번지에 2를 저장하라
  3. M(102) ⬅ M(100) + M(101)
  4. M(150) ⬅ 1
  5. M(151) ⬅ 2
  6. M(152) ⬅ 3

주의할 부분은 3번 명령어는 1번과 2번 명령어가 실행 종료되어야 올바른 결과값을 도출할 수 있다는 것이다.

이 소스코드를 CPU가 순차적으로 처리한다면 다음과 같다.

순차적 명령어 처리 기법

 

하지만 명령어들 중에 서로 의존성이 전혀 없는, 순서를 바꾸어 처리해도 무방한 명령어들이 있다.

  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)

다음과 같이 명령어 처리 순서를 변경했을 때 파이프라인을 그려보면 아래 그림과 같다.

순차적으로 명령어를 처리할 때보다 더 효율적으로 처리하는 것을 알 수 있다.

이처럼 파이프라인의 중단을 방지하기 위해 명령어를 순차적으로 처리하지 않는 명령어 병렬 처리 기법을 비순차적 명령어 처리 기법이라고 한다.

비순차적 명령어 처리 기법


CISC와 RISC

위에서 언급한 명령어 파이프라이닝과 슈퍼스칼라 기법을 CPU에 적용하려면 명령어가 파이프라이닝에 최적화되어 있어야 한다. 파이프라이닝 하기 쉬운 명령어란 무엇일까?

 

먼저 ISA라는 용어를 알아야한다. ISA는 CPU가 이해할 수 있는 명령어들의 모음이다. ISA가 다르다는 건 CPU가 이해할 수 있는 명령어가 다르다는 것이고 이해할 수 있는 명령어가 다르면 어셈블리어도 달라진다.

예를 들어 인텔 CPU와 애플 CPU는 ISA가 서로 다르기 때문에 인텔 컴퓨터와 아이폰은 서로의 명령어를 이해할 수 없다.

 

CPU가 이해하는 명령어들이 다르다는 것은 제어장치가 명령어를 해석하는 방식, 사용되는 레지스터의 종류와 갯수, 메모리 관리 방법 등 많은 것이 다르다는 의미이다. 그리고 이는 CPU 하드웨어 설계에 영향을 미친다.

ISA는 CPU가 이해할 수 있는 언어임과 동시에 CPU를 비롯한 하드웨어가 수많은 명령어로 이루어진 소프트웨어를 어떻게 이해할지에 대한 약속이라고 볼 수 있다.

 

이것을 CPU 성능 향상과 연결지어 설명하면 명령어 병렬 처리 기법에 유리한 ISA가 있고 그렇지 않은 ISA가 있다는 것이다.


 

CISC와 RISC는 CPU 설계 방식의 종류이다.

CISC는 Complex Instruction Set Computer의 약자로 복잡한 명령어 집합을 활용하는 컴퓨터를 의미한다.

CISC는 명령어의 형태와 크기가 다양한 가변 길이 명령어를 활용한다. 이 말은 상대적으로 적은 수의 명령어로도 프로그램을 실행할 수 있다는 것을 의미한다.

이는 메모리 공간을 절약할 수 있다는 장점이 있지만 명령어가 크고 복잡하기에 규격화가 어려워 파이프라이닝에 적합하지 않다. 그리고 대다수의 복잡한 명령어는 그 사용 빈도가 낮다.

 

CISC의 한계점을 해결하기 위해 등장한 것이 Reduced Instruction Set Computer의 약자인 RISC이다.

RISC는 원할한 파이프라이닝을 위해 명령어의 길이와 수행 시간이 짧고 규격화되어 있으며 자주 사용되는 기본적인 명령어를 작고 빠르게 만드는 것에 중점을 두었다. 따라서 CISC와 반대로 고정 길이 명령어를 사용한다.

그리고 RISC는 메모리에 직접 접근하는 명령어를 load, store 두 개로 제한한다. 메모리 접근을 단순화하고 최소화하는 것이다. 대신 레지스터를 적극 활용한다.

 

아래 코드는 같은 코드를 CISC로 설계된 x86-64 CPU로 처리한 어셈블리어와 RISC로 설계된 ARM CPU로 처리한 어셈블리어를 비교한 것이다. CISC가 RISC보다 적은 명령어로 코드를 수행하는 것을 알 수 있다.

int main(void) {
  int a = 1;
  int b = 2;
  int c = a + b;
  return 0;
}

// x86-64 (CISC)
main :
	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 (RISC)
main :
	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

 

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