본문 바로가기
CS

[CS] 명령어 병렬 처리 기법

by seonggu 2023. 2. 16.

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

 

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

 

 

1️⃣ 명령어 파이프 라인

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

 

1) 명령어 인출(Instruction Fetch)

2) 명령어 해석(Instruction Decode)

3) 명령어 실행(Execute Instruction)

4) 결과 저장(Write Back)

 

💥 이 단계는 책에 따라서 다른 것 같다. 명령어 인출 → 명령어 실행으로 나누기도 하고, 명령어 인출 → 명령어 해석 → 명령어 실행 → 메모리 접근 → 결과 저장으로 나누기도 한다고 한다.

 

 

여기서 중요한 점은 같은 단계가 겹치지 않으면 CPU는 '각 단계를 동시에 실행할 수 있다'는 것이다.

✅ 예를 들어 CPU는 한 명령어를 '인출'하는 동안에 다른 명령어를 '실행'할 수 있고, 한 명령어가 '실행'되는 동안에 연산 결과를 '저장'할 수 있다.

명령어 파이프라인을 사용하지 않고 모든 명령어를 순차적으로 처리한다면 인출 → 해석 → 실행 → 저장을 한다면 명령어 처리를 효율적으로 할 수 없다.

하여 예시처럼 명령어를 겹쳐서 수행하면 명령어를 하나하나 실행하는 것보다 효율적으로 처리할 수 있을 것이다.

 

이처럼 마치 공장 생산과 같이 명령어들을 명령어 파이프라인(instruction pipeline)에 넣고 동시에 처리하는 기법을 명령어 파이프라이닝(instruction pipelining)이라고 한다.

 

 

파이프라이닝이 높은 성능을 가져오긴 하지만, 특정 상황에서 성능 향상에 실패하는 경우도 있다.

이러한 상황을 파이프라인 위험(pipeline hazard)라고 부른다. 파이프라인 위험에는 크게 데이터 위험, 제어 위험, 구조적 위험이 있다.

 

ⓐ 데이터 위험

데이터 위험(data hazard)은 명령어 간 '데이터 의존성'에 의해 발생한다.

모든 명령어를 동시에 처리할 수는 없다. 어떤 명령어는 이전 명령어를 끝까지 실행해야만 비로소 실행할 수 있는 경우도 있다. 예를 들어서 두 명령어를 보면,

(참고 : 왼쪽 레지스터에 오른쪽 결과를 저장하라)

 

명령어 1: R1 ← R2 + R3 // R2 레지스터 값과 R3 레지스터 값을 더한 값을 R1 레지스터에 저장
명령어 2: R4 ← R1 + R5 // R1 레지스터 값과 R5 레지스터 값을 더한 값을 R4 레지스터에 저장

 

예시에서 명령어 1을 수행해야만 명령어 2를 수행할 수 있다. 즉, R1에 R2+R3 결괏값이 저장되어야 명령어 2를 수행할 수 있다. 만약 명령어 1 실행이 끝나기 전에 명령어 2를 인출하면 R1에 R2 + R3 결괏값이 저장되기 전에 R1 값을 읽어 들이므로 원치 않은 R1 값으로 명령어 2를 수행한다. 따라서 명령어 2는 명령어 1의 데이터에 의존적이다. 이처럼 데이터 의존적인 두 명령어를 무작정 동시에 실행하려고 하면 파이프라인이 제대로 작동하지 않는 것을 '데이터 위험'이라고 한다.

 

ⓑ 제어 위험

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

기본적으로 프로그램 카운터는 '현재 실행 중인 명령어의 다음 주소'로 갱신된다. 하지만 프로그램 실행 흐름이 바뀌어 명령어가 실행되면서 프로그램 카운터 값에 갑작스러운 변화가 생긴다면 명령어 파이프라인에 미리 가지고 와서 처리 중이었던 명령어들은 쓸모가 없어진다. 이를 '제어 위험'이라고 한다.

 

참고로 이를 위해 사용하는 기술 중 하나가 분기 예측(branch prediction)이다. 분기 예측은 프로그램이 어디로 분기할지 미리 예측한 후 그 주소로 인출하는 기술이다.

 

ⓒ 구조적 위험

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

구조적 위험은 자원 위험(resource hazard)라고도 부른다.

 

 

2️⃣ 슈퍼 스칼라

파이프라이닝은 단일 파이프라인으로도 구현이 가능하지만, 오늘날 대부분의 CPU에서는 여러 개의 파이프라인을 이용한다. 이처럼 CPU 내부에 여러 개의 명령어 파이프라인을 포함한 구조를 슈퍼 스칼라(superscalar)라고 한다.

명령어 파이프라인을 하나만 두는 것이 마치 공장 생산 라인을 한 개 두는 것과 같다면, 슈퍼스칼라는 공장 생상 라인을 여러 개 두는 것과 같다.

 

슈퍼스칼라 구조로 명령어 처리가 가능한 CPU를 슈퍼스칼라 프로세서 또는 슈퍼스칼라 CPU라고 한다. 슈퍼스칼라 프로세서는 매 클럭 주기마다 동시에 여러 명령어를 인출할 수도, 실행할 수도 있어야 한다. 멀티스레드 프로세서는 한 번에 여러 명령어를 인출하고, 해석하고, 실행할 수 있기 때문에 슈퍼스칼라 구조를 사용할 수 있다.

 

슈퍼스칼라 프로세서는 이론적으로 파이프라인 개수에 비례하여 프로그램 처리 속도가 빨라진다. 하지만 파이프라인 위험 등의 예상치 못한 문제가 있어 실제로는 반드시 파이프라인 개수에 비례하여 빨라지지는 않는다. 이 때문에 슈퍼스칼라 방식을 차용한 CPU는 파이프라인 위험을 방지하기 위해 고도로 설계되어야 한다. 여러 개의 파이프라인을 이용하면 하나의 파이프라인을 사용할 때보다 데이터 위험, 제어 위험, 자원 위험을 피하기 더욱 까다롭기 때문이다.

 

 

3️⃣ 비순차적 명령어 처리

비순차적 명령어 처리(OoOE; Out-of-order execution)은 보통 OoOE로 줄여서 말한다. 이 기법은 오늘날 CPU 성능 향상에 크게 기여한 기법과 대부분의 CPU가 차용한 기법이다.

 

비순차적 명령어 처리 기법은 이름에서도 알 수 있듯 명령어들을 순차적으로 실행하지 않는 기법이다. 파이프라이닝, 슈퍼스칼라 기법은 모두 여러 명령어의 순차적인 처리를 상정하는 방법이었다. 이는 프로그램을 위에서 아래로 차례차례 실행하는 방법이다. 하지만 파이프라인 위험과 같은 예상치 못한 문제들로 인해 이따금씩 명령어는 곧바로 처리되지 못한다. 만약 모든 명령어를 순차적으로 처리한다면 이런 예상치 못한 상황에서 명령어 파이프라인은 멈춰버리게 된다.

 

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

(참고 : 메모리 N번지에 M을 저장은 M(N) ← M으로 표기함)

 

M(100) ← 1 // 1번
M(101) ← 2 // 2번
M(102) ← M(100) + M(101) // 3번
M(150) ← 1 // 4번
M(151) ← 2 // 5번
M(152) ← 3 // 6번

 

여기서 3번 명령어를 실행하기 위해서는 M(100) 값은 물론 M(101) 값이 결정되어야 하기에 1번과 2번 명령어 실행이 끝날 때까지 기다려야 한다. 

이 명령어들을 순차적으로 실행되는 CPU를 실행하면 2번 명령어 실행이 끝날 때까지 3, 4, 5, 6번 명령어들은 기다린다.

 

그런데 한번 생각을 바꾸게 된다면 앞의 코드를 이루는 명령어들 중에 서로 데이터 의존성이 전혀 없는, 순서를 바꿔 처리해도 수행 결과에 영향을 미치지 않는 명령어들이 있다.

 

M(100) ← 1 // 1번
M(101) ← 2 // 2번
M(150) ← 1 // 4번
M(151) ← 2 // 5번
M(152) ← 3 // 6번
M(102) ← M(100) + M(101) // 3번

3번은 명령어와 순서를 바꾸어 실행해도 그게 문제 될 것이 없다. 이렇게 순서를 바꿔 실행하면 순차적으로 명령어를 처리할 때보다 더 효율적으로 처리된다. 이렇게 명령어를 순차적으로만 실행하지 않고 순서를 바꿔 실행해도 무방한 명령어를 먼저 실행하여 명령어 파이프라인이 멈추는 것을 방지하는 것을 비순차적 명령어 처리 기법이라고 한다.

 

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

M(100) ← 1 // 1번
M(101) ← 2 // 2번
M(102) ← M(100) + M(101) // 3번
M(103) ← M(102) + M(101) // 4번
M(104) ← M(100) // 5번

위 코드에서 3번 명령어와 1번 명령어의 순서를 바꿀 수는 없다. 3번 명령어를 수행하려면 반드시 M(100) 값이 결정되어야 한다. 마찬가지로 4번 명령어와 1번 명령어 순서를 바꿀 수 없다. 1번 명령어를 토대로 3번 명령어가 수행되고, 3번 명령어를 토대로 4번 명령어가 수행되어야 한다.

 

하지만 위 코드에서 4번 명령어와 5번 명령어는 순서를 바꾸어 실행할 수 있다. 다시 말하면 두 명령어는 어떤 의존성도 없기에 순서를 바꿔도 전체 프로그램의 실행 흐름에는 영향이 없다.

M(100) ← 1 // 1번
M(101) ← 2 // 2번
M(102) ← M(100) + M(101) // 3번
M(104) ← M(100) // 5번
M(103) ← M(102) + M(101) // 4번

그렇다면 위의 코드처럼 순서를 바꿔서 실행할 수 있다.

 

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

 

 

 

참고자료 -

'CS' 카테고리의 다른 글

[CS] RAM의 특징과 종류  (0) 2023.02.23
[CS] CISC와 RISC  (0) 2023.02.21
[CS] 클럭과 코어, 스레드  (0) 2023.02.14
[CS] 하드웨어 인터럽트  (0) 2023.02.09
[CS] 명령어 사이클과 인터럽트  (0) 2023.02.07