TIL

[TIL/혼공컴운] 2025/01/16

본 글은 이전 글의 내용에 의존합니다.reference: https://velog.io/@minkwan/TIL%ED%98%BC%EA%B3%B5%EC%BB%B4%EC%9A%B4-20250115 CPU 성능 향상 기법 ✍️CPU에서 성능 향상이라는 주제의 본질은

2025년 1월 16일6min read

본 글은 이전 글의 내용에 의존합니다.

reference: https://velog.io/@minkwan/TIL%ED%98%BC%EA%B3%B5%EC%BB%B4%EC%9A%B4-20250115

CPU 성능 향상 기법 ✍️

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

1-1. 클럭 🌿

CPU에서 성능 향상이라는 주제의 본질은 생각보다 단순하다. 조금이라도 더 빠르게 명령어를 처리하는 것이고, 이는 결국 '속도'에 관한 문제로 귀결된다. 따라서 CPU에서 '속도 단위'로 간주되는 '클럭 속도'에 대해 학습할 필요가 있다.

클럭 속도는 헤르츠(Hz) 단위로 측정하게 된다. 1초에 몇 번의 클럭이 반복되는가를 나타내는 지표다. 인텔의 공식 홈페이지에서 제공한 자료를 살펴보면, base가 3GHz, 최대 속도는 5.8GHz 임을 확인할 수 있는데, 이는 1초에 클럭이 기본 30억 번, 최대 58억 번 반복된다는 것을 뜻한다. 너무 큰 숫자라 크게 와닿지는 않는다는 느낌을 받는다.

그런데 우리는 지금 '성능 향상'이라는 큰 줄기로부터 클럭에 관한 이야기를 시작한 것이다. 그렇다면 클럭 속도를 높이면 반드시 CPU 속도가 빨라지냐? 그건 아니다. 발열 이슈를 피할 수가 없다. 이와 관련해서 최대 클럭 속도를 강제로 끌어올리는 기법을 ``오버클럭킹(overclocking)``이라고 한다.

클럭 속도를 높이는 것이 CPU를 빠르게 만드는 것은 사실이지만, 클럭 속도만으로 CPU의 성능을 올리는 것에는 한계가 있다는 말이 하고 싶었다.

1-2. 코어 / 멀티 코어 🌿

클럭 속도를 높이는 것 외에 CPU 성능을 높이는 방법에는, 코어와 스레드 수를 늘리는 방법이 있다.

코어를 알아보기 위해서는 CPU가 무엇인지 재정의할 필요가 있다.

과거의 CPU에 대한 정의```: 명령어를 실행하는 부품, 원칙적으로 하나만 존재.

코어가 두 개 이상이면 멀티 코어라고 부른다. 위 에브리타임 이미지에서 은둔 고수가 표현했듯이, 코어의 수는 노예의 수와 동일한 의미를 갖는다. 노예보다는 조원이라는 비유를 해보자.

코어가 많으면 반드시 그에 비례하여 CPU 연산 속도가 증가할까? 정답은 아니다. 혼자서 과제를 하는 것보다 둘이 하면 더 빠르고 효율적으로 일을 처리할 수 있지만, 4명이라고 과제를 수행하는 속도가 4배 더 빨라지지는 않는다. 그랬으면, 전국의 대학생들이 조별 과제를 혐오할 이유가 없다.

코어를 통해 말하고 싶은 것은, 코어의 절대적인 숫자의 증가보다는, ``코어가 처리할 명령어들을 얼마큼 적절하게 분배할 수 있는가``가 핵심이라는 것이다.

1-3. 스레드 / 멀티 스레드 🌿

은둔 고수는 스레드를 노예의 손 개수로 비유했다. 진짜 미친 사람이다(positive).

만약에 어떤 컴퓨터 스펙이 2코어 4스레드라면, 명령어를 처리하는 부품이 두 개이고, 한 번에 네 개의 명령어를 처리할 수 있는 CPU 임을 의미한다. 하나의 코어로 n가지 명령어를 동시에 처리하는 CPU를 멀티스레드 프로세서 또는 멀티스레드 CPU라고 부른다.

다만, 위 이미지에서 하드웨어적 스레드와 소프트웨어적 스레드를 구분한 이유는, CPU에서 사용되는 스레드와 프로그래밍에서 사용되는 스레드의 용례가 다르기 때문이다.

프로그래밍 언어나 운영체제에서 접하게 되는 스레드는, 하나의 프로그램에서 독립적으로 실행되는 단위를 의미한다. 하나의 프로그램이 실행되는 과정에서 동시에 여러 부분이 실행되는 상황으로 이해할 수 있다.

그러니까, ``1코어 1스레드로 구성된 CPU라고 할지라도 소프트웨어적 스레드를 수십 개 실행할 수 있다``. 스레드라는 단어의 용법을 정확히 이해하지 않으면 위에 언급한 말이 아리송하게 다가올 수밖에 없다.

오늘 얘기할 스레드는 하드웨어적 스레드다. 그렇다면 하나의 코어는 어떻게 n가지의 명령어를 동시에 처리할 수 있는 것일까? 답은 레지스터에 있다.

프로그램 카운터, 스택 포인터 등 명령어를 처리하기 위한 핵심적인 레지스터를 군집화해서 이러한 '세트'를 여러 개 갖고 있으면 멀티 스레딩이 가능해진다. 명령어를 독립적으로(+병렬적으로) 처리할 수 있기 때문이다.

하지만 메모리 입장에서는, 하드웨어 '스레드'는 한 번에 하나의 명령어를 처리하는 'CPU'나 다름없다. 무슨 말이냐면, 2코어 4스레드가 한 번에 네 개의 명령어를 처리한다고 한들 메모리 입장에서는 마치 CPU 자체가 네 개 있는 것처럼 보이기 때문이다. 이러한 이유로 하드웨어 스레드를 논리 프로세서라고 부르기도 한다. 컴퓨터 공학에서는 눈에 안 보이면 '논리'라는 단어를 붙이곤 하는 패턴이 있는 것 같다.

요약은 굳이 하지 않겠다. 글을 한 번 부담 없이 읽고, 에타 은둔 고수 형님? 혹은 누님?의 비유를 다시 보면, 머리에 빛이 들어오는 느낌을 받을 수 있다.

2. 명령어 병렬 처리 기법 ⚙️

2-1. 명령어 파이프라인 🌿

빠른 CPU를 만들기 위해서 ``높은 클럭 속도 + 멀티 코어 + 멀티 스레드``를 지원하는 것도 중요하지만, CPU가 놀지 않고 시간을 알뜰하게 쓰는 것도 중요하다. 환경이 완벽해도 본인이 움직이지 않으면 달라지지 않는다.

CPU가 한시도 쉬지 않고 작동하게끔 하는 기법을, 명령어 병렬 처리 기법이라고 한다. 우선 명령어 병렬 처리 기법 중 명령어 파이프라인에 대해 살펴보자.

명령어 파이프라인이란, 명령어를 병렬 처리하는 기법 또는 구조를 말한다. 위 이미지 자체가 명령어 파이프라인이라고 볼 수 있다.

파이프라이닝이 높은 성능을 가져오기는 하지만, 특정 상황에서는 실패하는 경우도 있고 이를 파이프라인 위험이라고 부른다. 파이프라인 위험은 다음과 같이 나뉜다.

가장 먼저 ``데이터 위험``은 데이터 의존성에 의해 발생한다. 명령어 1이 R2와 R3을 더해서 R1에 저장하라는 명령어고, 명령어 2가 R4에 R1과 R5 값을 더해서 저장하라는 명령어라면, 명령어 1에 대한 순서가 보장되어야 한다. 즉, 데이터 간의 의존성이 있는 상황이다. 이렇게 데이터 간 의존성이 있을 때 무작정 동시에 각기 다른 명령어를 실행하려다가 파이프라인이 제대로 작동하지 않는 것을 '데이터 위험'이라고 부른다.

다음으로 ``제어 위험``이다. 제어 위험은 프로그램 카운터의 갑작스러운 변화에 의해 촉발된다. 프로그램 카운터는 기본적으로, 현재 실행 중인 명령어의 다음 주소로 갱신된다. 그런데 갑자기 "실행해 보니까 60번지로 분기해야겠네?"라고 하면 명령어 파이프라인에 미리 가지고 와서 처리 중이던 명령어들은 아무 쓸모가 없어지는데, 이러한 위험을 제어 위험이라고 한다.

마지막으로 ``구조적 위험``은 명령어들을 겹쳐 실행하는 과정에서 서로 다른 명령어가 동시에 동일한 ALU, 동일한 레지스터를 사용하려 할 때 발생하는 위험이다. 그래서 구조적 위험은 자원 위험이라고 부르기도 한다.

2-2. 슈퍼 스칼라 🌿

슈퍼 스칼라는 간단하다. 여러 개의 파이프라인을 포함한 구조를 '슈퍼 스칼라'라고 부른다.

슈퍼스칼라 프로세서는 ``파이프라인이 n개인 구조``다. 이론적으로 파이프라인 개수에 비례하여 프로그램 처리 속도가 빨라지는 것은 사실이다. 하지만 우리는 앞서 파이프라이닝에서 발생할 수 있는 데이터 위험, 제어 위험, 구조적 위험에 대해 살펴봤다.

파이프라인이 늘어날수록 이러한 위험에 대한 소요도 증가하기에, 실제로 반드시 파이프라인 개수에 비례하여 프로그램 처리 속도가 빨라진다고 단정할 수는 없는 것이다.

그런데 그냥 멀티 파이프라인이라고 하면 되지 '슈파 스칼라'라고 명명한 것이 굉장히 킹받는다. 이것도 이유가 있을 것이라 믿는다.

2-3. 비순차적 명령어 처리 🌿

명령어를 병렬 처리하는 기법 중 오늘 소개할 마지막 기법은, 비순차적 명령어 처리(Out-of-order-execution)이다. 비순차적 명령어 처리는 한마디로 ``합법적인 새치기``다.

명령어 파이프라이닝, 그리고 그 확장 버전인 슈퍼 스칼라는 결국 명령어를 순차적으로 처리한다. 다만, 병렬적으로 처리할 뿐이다. 비순차적 명령어 처리는 다음과 같다.

명령어를 순차적으로만 실행하지 않고, 순서를 바꿔 실행해도 무방한 명령어를 먼저 실행하여 명령어 파이프라이닝의 정지를 방지하는 기법. 그것이 바로 비순차적 명령어 처리 기법이다.

다시 정리하면, 비순차적 명령어 처리 기법이란, 파이프라인의 중단을 방지하기 위해 명령어를 순차적으로만 처리하지 않는 명령어 병렬 처리 기법이다.

3. CISC와 RISC ⚙️

3-1. 명령어 집합 🌿

명령어 병렬 처리 기법(파이프라이닝, 슈퍼 스칼라)을 효과적으로 사용하려면, 명령어 자체가 파이프라닝하기 쉽게 생겨야 한다. 그런데 세상에는 수많은 CPU 제조사들이 있고, CPU마다 규격과 기능, 만듦새가 다 다르다. 따라서 명령어의 세부적인 생김새나 주소 지정 방식 등도 조금씩 차이가 있다.

즉, CPU마다 ISA(명령어 집합 구조)가 다르다.

동일한 소스 코드를 작성하고, ISA가 다른 컴퓨터에서 어셈블리어로 컴파일하면, 어셈블리어 형태가 다르게 나오는 것을 확인할 수 있다. 만약 궁금해 미치겠다면 혼공컴운을 구매하실 것을 권장한다.

정리하자면 ISA는 CPU의 언어이자, ``하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속``이다.

3-2. CISC 🌿

CISC는 Complex Instruction Set Computer의 약자고, 한국어로 '복잡한 명령어 집합을 사용하는 컴퓨터'라고 할 수 있다. x86이나 x86-64는 대표적인 CISC 기반의 ISA이다.

CISC는 어쨌든 CPU 설계 방식이다. CISC는 ``가변 길이 명령어``를 활용한다.

CISC 명령어 집합은 복잡하고 다양한 기능을 제공하기에 적은 수의 명령으로 프로그램을 동작시키고 메모리를 절약할 수 있다. 이는 메모리를 최대한 아끼며 개발해야 했던 시절에 인기가 높았다.

하지만 동시에, 명령어의 규격화가 어려워 파이프라이닝이 어렵다는 단점이 있다. 명령어 파이프라이닝이 제대로 동작하지 않는다는 것은 현대 CPU에서 아주 치명적인 약점이다. 게다가 대다수의 복잡한 명령어는 그 사용 빈도가 낮다.

아래의 이미지를 참고하자.

3-3. RISC 🌿

RISC는 Reduced Instruction Set Computer다. 이름부터 뭔가 많이 줄였음이 예상된다. 원활한 파이프라이닝을 위해 명령어의 길이와 수행 시간을 줄이고 규격화했다. 그리고 복잡한 명령어를 추가하기보다는 자주 쓰이는 기본적인 명령어를 작고 빠르게 만드는 것에 집중한 설계 방식이다.

RISC는 ``고정 길이 명령어``를 사용한다는 말과 동일하다. 하나의 명령어를 1클럭 내외로 실행하기 때문에, 명령어 파이프라이닝에 최적화되어 있다고 평가할 수 있다.

그리고 RISC는 메모리에 직접 접근하는 명령어를 load, store 두 개로 제한할 만큼 메모리 접근을 단순화하고 최초화를 추구한다. 이러한 이유로 RISC를 load-store 구조라고 칭하기도 한다. 정리하자.

내일은 SQL! (책 이름임)