컴퓨터구조

[컴퓨터 구조] 4.11 명령어를 통한 병렬성, ILP(Instruction Level Parallelism)

ima9ine4 2023. 12. 14. 23:58
728x90

'컴퓨터 구조 및 설계 MIPS edition 제 6판' 교재와 국민대학교 임은진 교수님의 강의를 바탕으로 정리 및 요약한 글입니다.
정리 과정에서의 오류 및 오타가 있을 수 있습니다 :)


파이프라이닝은 명령어들 사이의 병렬성을 이용한다. 이를 ILP(Instruction Level Parallelism, 명령어 수준 병렬성)이라고 한다.
병렬성을 증가시키면 성능이 좋아질 것이므로 ILP를 증가시켜야 한다. 그 방법으로는 두 가지가 있다.

1. Deeper Pipeline
첫 번째 방법은 파이프 라인의 깊이를 증가시켜 더 많은 명령어들을 중첩시키는 것이다.
그렇게 되면 stage 당 일이 줄어들기 때문에 clock cycle이 더 짧아져 성능이 좋아질 수 있다.
교재의 비유에 따르면 이 방법은 세탁, 헹굼, 탈수 이렇게 총 3가지 기능을 하는 하나의 세탁기를 각각 다른 기계로 나누는 것이다. 세탁을 하는 기계, 헹굼을 하는 기계, 탈수를 하는 기계 이렇게 3가지 기계가 각각 자신의 일을 수행한다. 그러면 하나의 세탁기가 하는 일을 세 기계가 나눠서 하게 된다. 그래서 stage 당 일이 줄어들어 clock cycle이 짧아진다고 말하는 것이다.

2. Multiple issue
두 번째 방법은 다중 내보내기(Multiple issue)이다.
이 방법은 세탁기를 나누는 것이 아니라 세탁기를 3개 사용하는 것이다. 이렇게 여러 개의 명령어를 한꺼번에 내보내면 명령어 실행 속도가 클럭 속도보다 빨라질 수 있다. 즉, CPI < 1 일 수 있다. 동시에 CPI의 역수 값인 IPC(Instruction Per Clock cycle)는 1보다 커지게 될 수 있다.
하지만 Instruction을 동시에 여러 개 수행하게 되면, dependency가 커질 것이고 이는 pipeline stall으로 이어질 수 있다는 문제점이 있다.


Multiple Issue를 구현하는 방법은 2가지가 있다. Static Multiple Issue와 Dynamic Multiple Issue인데, 하나씩 살펴보도록 하자.

2-1. Static Multiple Issue (정적 다중 내보내기)
이는 컴파일 시에 컴파일러에 의해 여러가지 결정들이 이루어지는 방식이다.
컴파일러가 같이 이슈가 될 instruction들을 그룹으로 묶어주는데 이 묶음을 issue packet이라고 한다. issue packet은 여러 개의 연산자를 갖는 큰 명령어 하나로 생각할 수 있다. 이 관점에서 VLIW(very long instruction word), 매우 긴 명령어라는 명칭이 붙었다.

Static Multiple Issue에서는 컴파일러가 해저드를 제거하는 데 책임이 있다. 컴파일러는 issue packet 안의 명령어들을 reorder하여 packet 내에 의존성이 없도록 한다. 필요하다면 nop을 삽입한다.

그렇다면 Static Multiple Issue에 대해 더 알아보기 위해서 2개의 명령어를 동시에 내보내는 프로세서로 예를 들어보자.

Static Dual Issue를 구현한 데이터회로

위 그림은 Static Dual Issue를 사용하는 회로이다. 파란 색으로 회로에 추가된 부분이 있다. 명령어를 2개 내보내기 위해서 추가한 것이다. 그런데 이런 식으로 명령어를 2개씩 내보내다보면 데이터 해저드의 문제를 예상할 수 있다.

예를 들어
add $t0, $s0, $s1
load $t2, 0($t0)
위 두 개의 명령어는 같이 수행될 수 없다.
기존의 single issue였다면 Forwarding으로서 stall을 피했겠지만, dual issue에서는 Forwarding이 해결책이 될 수 없다. 이러한 문제를 막기 위해서는 병렬성을 효과적으로 활용해야 하는데, 여기서는 컴파일러에게 코드 스케줄링이 요구된다.

컴파일러의 코드 스케줄링을 예제를 통해 알아보자.

위와 같은 순환문에서 stall을 최대한 피할 수 있게 재정렬하려면 어떻게 해야할까? 정답은 아래의 그림과 같다.

Static Dual Issue를 통해 코드 스케줄링한 결과

첫 3개의 명령어와 마지막 2개의 명령어 사이에 데이터 종속성이 있다. 따라서 해당 명령어들은 같이 내보낼 수가 없으므로 위와 같은 방법이 최선이다.
Cycle 수는 4, Instruction의 수는 5개이므로 CPI = 4/5 = 0.8이고, IPC = 5/4 = 1.25이다.
Static Dual Issue에서 peak CPI는 0.5이고 peak IPC는 2 이므로 좋은 성능은 아니다.

이러한 순환문에서 더 좋은 성능을 얻기 위한 컴파일러 기법은 Loop Unrolling이다. 순환문의 body를 여러 벌 복사해서 서로 다른 반복에 속한 명령어들을 같이 스케줄링하는 방식이다. 이 과정에서 컴파일러는 Register Renaming(레지스터 재명명)을 수행하기도 한다. 이는 'anti-dependencies'를 피하기 위함이다. 진짜 데이터 존속성은 아니지만 잠재적으로 해저드의 원인이 되거나 컴파일러가 유연하게 코드 스케줄링하는 것을 방해하는 종속성을 없애자는 것이다. 예시를 통해 더 자세히 살펴보자.

lw $t0, 0($s1)
addu $t0, $t0, $s2
sw $t0, 4($s1)

 

이 3개의 명령어에서는 모두 $t0를 사용하지만 꼭 $t0를 사용해야할 필요는 없다. 이를 'anti-dependencies'라고 하는 것이다. 실제 데이터 종속성이 아닌, 순전히 같은 이름을 반복 사용함으로써 강요되는 순서이다. 따라서 컴파일러는 레지스터들을 재명명하여 명령어들을 독립적으로 만들고, 더 효율적으로 코드스케줄링할 수 있다. 

Loop Unrolling이 동작하는 과정의 예시

그렇게 Loop Unrolling과 코드 스케줄링이 끝나면 성능이 더 좋아지지만(IPC는 2에 더 가까워지지만), 더 많은 레지스터들을 사용해야하고 코드 사이즈가 커진다.

2-2. Dynamic Multiple Issue (동적 다중 내보내기)

Multiple Issue를 구현하는 또 다른 방법인 Dynamic Multiple Issue에 대해서 알아보자.
Dynamic Multiple Issue는 'Superscalar(수퍼스칼라)'라고도 한다.

제일 간단한 수퍼스칼라 프로세서는 명령어를 순차대로(in-order) 내보내고, 주어진 클럭 사이클에 몇 개의 명령어를 내보낼지를 결정한다. 이런 프로세서에서 좋은 성능을 얻기 위해서는 컴파일러가 명령어를 스케줄하여 종속성 있는 명령어들의 위치를 멀리 떨어뜨리고 그렇게 하여 명령어 내보내기율을 증가시켜야 한다.

이렇게 컴파일러 스케줄링을 사용하기는 하지만 단순한 수퍼스칼라 프로세서와 VLIW 프로세서는 결정적인 차이가 있다. VLIW에서는 컴파일러가 잘 스케줄해야 프로그램이 제대로 실행되지만, 수퍼스칼라 프로세서는 컴파일러의 코드 스케줄링과 상관없이 하드웨어가 코드의 올바른 실행을 보장한다.

많은 수퍼스칼라 프로세서는 Dynamic Multiple Issue를 결정하는 기본 틀을 확장하여 동적 파이프라인 스케줄링(Dynamic Pipeline Scheduling)을 포함하고 있다. Dynamic Pipeline Scheduling은 이는 stall을 피할 수 있도록 명령어 실행 순서를 바꾸는 재정렬을 하드웨어가 지원하는 것이다.

동적 스케줄링 파이프라인의 세 가지 주요 유닛

위 그림에서 동적 스케줄링 파이프라인의 세 가지 주요 유닛을 확인할 수 있다.

1. 첫 번째 유닛은 명령어를 가져오고(fetch) 해독하고(decode) 각각의 명령어를 실행 단계의 해당 기능 유닛에 보낸다.
2. 각 기능 유닛은 대기 영억(reservation station)이라 불리는 버퍼를 가지고 있는데 이 대기 영역은 피연산자와 연산자를 가지고 있다. 버퍼에 모든 피연산자가 준비되고 실행할 기능 유닛이 준비되어 있으면 연산을 실행한다. 연산이 끝나면 그 결과는 결과 쓰기 유닛뿐만 아니라 이 결과를 기다리고 있는 대기 영역에도 보내진다.
3. 결과 쓰기 유닛은 결과값을 버퍼링하고 있다가 안전할 때 결과값을 레지스터 파일이나 메모리(sw 명령어의 경우)에 쓴다. 결과 쓰기 유닛에 있는 버퍼는 재정렬 버퍼(reorder buffer)라고 불리는데 피연산자들을 공급하는 역할을 한다.

이러한 과정은 명령어 인출 순서와 다르게 실행될 수 있기 때문에 이와 같은 실행형태를 out of order execution(비순차 실행)이라고 부른다. 실행할 수 없는 명령어 때문에 뒤의 명령어들이 기다리지 않게 하는 파이프라인 실행의 상황을 의미한다.

지금까지 Multiple Issue에 대해서 알아보았다. Multiple Issue는 ILP를 증가시키는데에 효과는 분명히 있지만 이상적이지는 않다. 제거하기 어려운 종속성도 있으며 어떤 병렬성은 제한된 window size(미리 볼 수 있는 명령어 개수의 크기)로 인해 활용하기 어렵다. 이러한 문제들로 인해 병렬성은 한계를 가지게 된다. 또한 메모리 지연의 문제도 있다.


마지막으로 '추정(Speculation)'에 대해 알아보자. 추정이란 더 많은 ILP를 찾아내고 이용하기 위한 기법이다. 컴파일러나 프로세서가 어떤 명령어의 결과를 예측하여 이 명령어에 종속적일 수 있는 다른 명령어들의 실행을 시작할 수 있게 한다.

Branch가 일어날지 안일어날지 에측하는 Branch prediction을 예로 들 수 있다. Branch 명령어의 결과를 추정한다면 Branch 명령어 뒤의 명령어들이 일찍 실행될 수 있다.

또 Load 명령어의 바로 앞에 있는 Store 명령어가 같은 주소를 참조하지 않는다고 추정하여, Load 명령어가 Store 명령어보다 먼저 실행될 수 있게 할 수 있다.

하지만 '추정'은 언제까지나 추정이므로 잘못될 수 있다. 따라서 모든 추정 기법은 실패 시 다시 되돌릴 수 있는 방법을 가지고 있어야한다. 이 취소 방법은 컴파일러에서 추정이 이루어지느냐, 하드웨어에서 이루어지느냐에 따라 다르다. 컴파일러의 경우 fix-up instruction들을 제공하고, 하드웨어의 경우 추정 결과가 틀렸음을 알 때까지 추정결과를 버퍼링한다. 이때 flush buffer를 이용한다. 


지금까지 ILP를 증가시키기 위한 방법에 대해서 알아보았다.

ILP를 증가시키기 위한 방법으로는 파이프라인을 더 잘게 나누는 방법과 Multiple Issue가 있고 Multiple Issue는 컴파일 시에 결정할 것인지(Static), 동적으로 실행 중에 이루어질 것인지(Dynamic)에 따라 두 가지 방법으로 구현할 수 있다.

또한 더 많은 ILP를 찾아내기 위한 방법으로 '추정'에 대해서도 배웠다.

반응형
LIST