본문 바로가기

CS/컴퓨터구조

[컴퓨터구조] ILP & superscalar processor(1)

현재 글은 컴퓨터 구조와 아키텍처 서적 및 대학에서 들은 강의를 기반으로 한다. 


ILP(Instruction Level Parallelism)

ILP는 instruction 수준에서 병렬적으로 처리할 수 있는 정도를 의미한다. 기본적으로 instruction은 컴파일러에 의해 분석된 머신 코드 수준에 해당하는데, 이 정도 레벨이 되면 상호 간에 의존성이 높아 동시에 병렬적으로 실행될 수 있는 비율 자체는 그리 높지 않다고 한다.

SuperScalar

 연구 결과에 따르면 instruction 레벨에서 데이터를 처리할 때 상수 : 스칼라: 벡터 관련 instruction의 비율은 대략 20 : 55 : 25 정도로 나타났다. 이는 실제 기기에서 instruction의 대부분을 스칼라 관련 instruction이 차지한다는 의미로, 이를 빠르게 처리할 수 있는 하드웨어 구조를 만드는 것이 프로세서의 전반적인 성능 향상에 큰 영향을 가져온다고 유추할 수 있다.

  superscalar은 이러한 발상을 기반으로 하여 여러 개의 instruction을 동시에 실행할 수 있도록 파이프라인 및 연산 유닛 등을 다중화하는 구조로, 동시에 여러 개의 instruction을 서로 다른 파이프라인에서 실행할 수 있다는 특징을 가진다. 간단하게 보면 파이프라인 자체를 여러 개 두고 실행하는 방식으로 생각할 수 있다.

superscalar의 instruction 처리 모습

 superscalar 머신은 동시에 여러개의 독립적인 instruction을 실행할 수 있으므로 좋아 보인다. 그러나 현실에서는 vector machine ( SGI cray 등 )이 먼저 개발되었다. 이는 scalar instruction에 규칙성이 존재하지 않기 때문이다. 

 vector instruction은 일반적으로 규칙성을 가지며, 각 instruction이 독립적으로 동작하므로 병렬적 실행이 쉽다. 예를 들어 그래픽 작업 중 각 픽셀에 대해 뷰 변환을 하는 과정에서 모든 픽셀은 동일한 연산을 수행하며, 다른 픽셀과 관계없이 연산이 성립한다. 따라서 이런 벡터 변수에 대한 작업의 하드웨어적 구현은 스칼라 변수에 대한 작업에 비해서는 쉬운 편이다. 반면 scalar instruction은 이러한 규칙성이 존재하지 않으며, 주변 instruction들에 대해 dependency를 가지는 경우가 많다. BRANCH에 의한 분기 등이 이에 대한 대표적인 예시에 속한다.

 superscalar 구조는 CISC, RISC에 관계없이 적용할 수 있다. 추가적인 연결, 장치의 추가 등을 요구하는 특성상 기존에도 복잡한 구조를 가지는 CISC와는 잘 어울리지 않는다고 생각할 수 있으나, superscalar 개념이 등장한 90년대 이후부터 새도 등장한 CISC 기반 장치가 거의 없기 때문에 실제로 이를 논하는 것은 큰 의미가 없다고 한다. CISC 기반으로 설계되는 장치 자체가 거의 없는 탓에 일반적으로 RISC 머신과 융합한다고 한다. 경우에 따라 Intel 사의 제품을 CISC라고 생각한다면 CISC 기반 superscalar 프로세서로 분류할 수 있긴 하다.

 슈퍼스칼라를 위해 고려해야 하는 부분

 컴퓨터에서 실행되는 instruction에는 스칼라의 비중이 절반 이상으로 상당히 높다. 그러나 scalar instruction 특성상 주변 명령들 사이에 dependency을 가지는 경우가 많기 때문에 실제로는 ILP(Instruction Level Parallelism)이 상당히 낮게 측정된다. 어떤 연구 결과에 따르면 아무리 슈퍼스칼라 머신에 추가적인 장치 및 파이프라인을 추가하더라도 속도 증가 비율은 8배를 넘을 수 없다고 한다. 

 따라서 슈퍼스칼라 머신의 효율을 높이기 위해서는 ILP을 높이기 위한 하드웨어 / 소프트웨어적 개선이 요구된다.

scalar instruction을 다루는 구조와 superscalar 구조의 비교. superscalar 구조에는 다수의 functional unit 및 파이프라인이 요구된다.
superscalar의 속도 증가 효과를 분석한 연구 결과들.

슈퍼스칼라의 한계

 superscalar machine에서 다루고자 하는 scalar instruction은 낮은 ILP를 가지고 있다. 이때 superscalar은 여러 개의 파이프라인을 동시에 동작하는 개념이므로 파이프라인에서 나타나는 여러 가지 dependency에 의한 문제들(파이프라인 해저드)이 동일하게, 심화되어 발생한다.

  • true data dependency (RAW)
  • anti dependency (WAR)
  • output dependency (WAW)
  • resource conflict (resource dependency)
  • procedural dependency (control dependency)

이러한 문제들을 줄이기 위해 컴파일러 및 하드웨어적 테크닉을 통해 보완할 필요가 있다.

superscalar에서 발생하는 다양한 dependency 문제들.


Superpipelined

 파이프라인을 좀 더 잘게 쪼개는 방식. 초기 RISC 머신에서 스테이지의 개수는 대략 4 ~ 6개로 나타난다. 이때 각 스테이지를 2개로 쪼개면 총 8 ~ 12 스테이지를 가지는 파이프라인이 되며, 이게 superpipeline에 속한다. 파이프라인의 이론적 속도 증가는 스테이지 개수 K에 비례하므로, 사이클의 길이를 줄이고 한 사이클에 처리 가능한 instruction의 개수를 늘림으로써 더 짧은 시간으로 전체 instruction을 실행할 수 있게 된다.

 쉽게 말하면 스테이지의 개수가 많은 파이프라인이다. 현대 컴퓨터들은 기본적으로 파이프라인의 스테이지 개수가 많으며, Intel 프로세서의 경우 거의 20개가 넘는 스테이지를 가지는 것으로 알려져 있다.

VLIW(Very Long Instruction Word)

VLIW의 instruction 처리 구조도

 일반적으로 메모리 I/O 동작은 프로세서에 비해 상당히 많은 시간을 요구한다. 이때 한 메모리 사이클에 가져올 수 있는 데이터의 크기는 메모리에 연결된 line의 개수에 비례하며, line의 개수가 늘어난다고 해도 메모리 I/O 동작에는 큰 영향을 주지 않기 때문에 컴퓨터의 line 개수를 늘려 메모리로부터 한 번에 읽어올 수 있는 데이터의 크기를 늘림으로써 I/O 횟수를 줄이자는 개념에서 등장한 것이 VLIW이다. 

 32개의 라인을 가진 메모리는 한번에 32bit 데이터를 주고받을 수 있다. 이때 라인의 개수를 128개로 늘리면 한 번에 128bit 데이터를 주고받을 수 있으므로 이론적으로 I/O 횟수를 4배 줄일 수 있게 된다. 또한 기존 32bit 기준으로 구성되어 있던 operation을 한 번에 4개 포함할 수 있으므로 128bit의 한 instructon에 4개의 operation을 포함하는 효과가 있다.

하나의 instruction(128bit)에 4개의 operation이 포함된 모습

 VLIW의 개념(한번에 메모리에서 많이 읽어오자)은 기존 마이크로 프로그래밍 수준에서도 사용되었다. 마이크로 프로그래밍은 control unit의 동작을 프로그램으로 작성하고, 이를 메모리에 올려 실행하는 방식으로 하드웨어 회로로 control unit을 구현하는 hard-wired 방식과 반대되는 개념이다. 메모리에 control unit의 동작을 적재하는 경우 하드웨어로 구현하는 방식에 속도가 상당히 저하되기 때문에 한 번에 여러 코드를 가져오기 위해 horizontal microinstruction format을 두었다고 한다.

 VLIW은 한번에 여러 operation을 메모리로부터 읽어 동시에 실행하는 방법으로, superscalar과 비슷한 수준의 하드웨어 요구사항을 가진다. 그러나 superscalar이 명령을 읽어 들여 런타임에 스케줄링을 수행(dynamic issue)하는 것과는 달리 컴파일러에 의해 컴파일 타임에 instruction들의 실행 순서를 결정할 수 있다는 특징을 가진다(static issue). 여기서 issue는 instruction을 디코드(decode)하고 실행(execute) 하기 위한 중간 단계를 의미한다.

 VLIW을 사용하는 경우 컴파일러가 소스코드를 분석하여 알아낸 instruction 사이의 dependency을 기반으로 instruction의  실행 순서를 정한다. 따라서 런타임에는 단순히 분석된 코드를 실행하면 된다. 이때 trace scheduling이라는 기법을 적용하여 코드를 생성한다. 컴파일러가 코드를 분석하는 단계에서는 branch가 어디로 분기될지 알 방법이 없다. 따라서 스케줄링 단계에서 branch의 두 분기에 해당하는 코드를 모두 생성해 두고, instruction을 모아 스케줄링을 진행한다. 이 과정에서 중복된 코드가 다수 생성될 수 있다.

trace scheduling

VLIW의 특징은 다음과 같다.

  • 디코딩 과정이 쉽다. 슈퍼 스칼라 머신의 경우 하드웨어 수준에서 런타임에 instruction 사이의 dependency을 체크하며 다음 실행할 instruction을 동적으로 정해야 하는 부담이 있으나, VLIW의 경우 컴파일러 수준에서 소스 코드를 정적 분석하여 명령들의 실행 순서를  컴파일 타임에 순서를 정해두고 실제 실행 단계에서는 컴파일된 코드를 실행하기만 하면 된다. 컴파일 수준에서 각 instruction의 dependency를 알 수 있는 VLIW가 superscalar에 비해 훨씬 쉬운 디코딩 과정을 가진다.
  • superscalar에 비해 더 많은 코드를 가진다. VLIW은 trace scheduling을 수행하는 과정에서 각 분기점에 대응되는 코드를 생성하므로 동일 범위에 대한 중복된 코드를 가질 수 있다. 따라서 VLIW을 채택하는 경우 더 많은 메모리가 요구될 수 있다. 반면 superscalar은 런타임에 instruction을 동적으로 평가하는 방식으로 동작하기 때문에 추가적인 코드가 생성되지 않는다. 
  • object code 레벨의 호환성이 낮다. VLIW의 가장 큰 문제점이다. 기본적으로 VLIW의 static issue는 컴파일러가 소스코드를 분석하여 해당 환경에 맞게 재배치하여 컴파일하는 방식으로 동작한다. 반대로 말하면 소스 코드가 제공되지 않는 경우 자신의 환경에 맞는 instruction 최적화가 불가능하다고 볼 수 있다. 이때 많이 사용하는 excel, word 등 상용 프로그램들의 대다수는 소스 코드 레벨 수준의 제품을 거의 판매하지 않는다. 이에 따라 VLIW 환경에서 사용할 수 있는 소프트웨어의 수가 적기 때문에 VLIW을 채택할 수 없다. 즉, VLIW의 개념은 좋지만 시장 환경이 이에 따라주지 않기 때문에 채택률이 낮아지게 된다.

 마지막 특징, 즉 상용 프로그램의 대부분이 소스 코드 레벨의 결과물을 제공하지 않는 특성에 의해 현재 시점에서 VLIW을 본격적으로 채택하는 경우는 드물다. VLIW을 채택하기 위해서는 좀 더 오픈소스 기반의 환경이 활성화되어야 할 것이다.


기본 파이프라인, superpipelined, superscalar을 비교한 그림


Design Issues

 superscalar에서 발생하는 여러가지 dependency 문제를 해결하고 ILP을 높이는 것이 중요하다.

  • Instruction Level Parallelism: data & procedural dependency 해결 필요.
  • Machine Parallelism: 병렬적으로 동작하는 파이프라인의 개수에 의해 좌우됨.

 ILP을 높이기 위해서 여러가지 개념 및 알고리즘을 적용할 필요가 있으며, 이를 위해 머신 수준의 병렬성이 높을 필요가 있다. 예를 들어 ILP가 2.5로 측정되었다면, 이를 위해 최소 4 ~ 6개의 파이프라인을 보유해야 한다.

Instruction Issue Policy

 superscalar machine에서 효율적으로 instruction을 실행하기 위해서는 서로 독립적으로 동작하는 명령들을 동시에 실행할 수 있도록 기존 코드의 순서를 변경하거나, 코드에 의한 결과가 반영되는 시점을 변경할 필요가 있다.

 Instruction Issue는 decode 된 instruction의 기능 유닛(ALU 등)에서의 실행을 시작하는(initiate) 단계로, 구체적으로는 decode 후 첫 번째 execute stage로 들어가는 시점을 의미한다.

 우리는 instruction issue에 대한 정책을 선택함으로써 보다 효율적인 파이프라인 구성이 가능해진다. 이때 3가지 순서에 대한 요소를 고려할 수 있다.

  • instruction이 fetch 되는 순서
    : 실행 전에 뭘 먼저 가져와야 할지 아는 것이 어색하기는 하나 나쁘지 않다.
  • instruction이 실행(execute) 되는 순서 [ issue ]
    : 실행 순서 자체를 변경하는 방법이다.
  • instruction의 내용이 레지스터 또는 메모리에 반영(update)되는 순서 [ completion ]
    : 실행되더라도 해당 결과가 반영되는 순서를 지정한다.

 이러한 요소를 기반으로 대략 4가지 정책을 수립할 수 있다.

반영 순서 \ 시작 순서 순서대로(in-order) 변경(out-of-order)
순서대로(in-order) in-order issue
in-order completion
in-order issue
out-of-order completion
변경(out-of-order) out-of-order issue
in-order completion
out-of-order issue
out-of-order completion

 위 정책 중 가장 효율적인 정책은 issue 및 completion 순서를 모두 변경할 수 있는 out-of-order issue out-of-order completion이다. 실제 실행 순서 및 반영 순서를 자유롭게 정할 수 있으므로 더 유연하게 instruction을 다룰 수 있다.


in-order issue & in-order completion

 명시된 순서 그대로 실행하는 방법이다. 효율성이 상당히 떨어지며, 여러 dependency을 회피하기 위해 상황에 따라 대기(stall)을 삽입하는 방식으로 동작한다.

Instruction 실행 순서

위 그림은 Instruction들이 실행되는 순서를 나타낸 것이다. 이때 전제 조건은 다음과 같다.

  • I1은 실행에 2 사이클을 요구한다.
  • I3I4는 동일한 functional unit을 사용하므로 conflict가 존재한다.
  • I5는 I4의 결과에 의존한다.
  • I5I6은 동일한 functional unit을 사용하므로 conflict가 존재한다.

3 사이클 시점에서 I1과 I3는 동시에 실행될 수 있다. 그러나 I1, I2, I3가 write 대상일 때 I3가 먼저 write 되는 일이 발생하면 in-order completion을 보장할 수 없으므로 I4는 4사이클에 실행된다. I3와 I4는 resource conflict가 존재하므로 순서대로 진행되며, 이후 I4에 대해 의존성을 가진 I5와 recource conflict가 있는 I6가 순차적으로 실행된다. 


in-order issue & out-of-order completion

 fetch된 순서대로 실행되되, 해당 결과가 반영되는 순서를 바꿀 수 있는 상황이다. 종료 순서가 바뀌면 상황에 따라 여러가지 dependency 문제가 발생할 수 있다.

R3 = R3 op R5
R4 = R3 + 1 
R3 = R5 + 1 (anti dependency)
R7 = R3 op R4 (true dependency)

output dependency 1,3 : 1이 실행되기 전에 3이 내용을 쓰면 안된다.
true dependency 1,2

anti dependency 2,3 : 2에서 R3을 읽기 전에 3이 실행되면 안된다.
true dependency 2,3,4

 위 코드에서는 간단한 명령 사이에서 발생하는 dependency들을 보여주고 있다. data dependency는 3가지 종류가 존재하며 이중 antidependency 및 output dependency의 경우 register renaming을 통해 다른 레지스터에 배정함으로써 동일 레지스터를 사용하는 문제를 해결할 수 있다.

instruction 실행 순서

위에서 보였던 instruction이 변경된 모습이다. 이전에는 in-order completion이 보장되어야 했기 때문에 I1과 I3가 동시에 실행될 수 없었다. 그러나 out-of-order completion 상황에서는 I1 및 I3가 동시에 실행될 수 있으므로 한 사이클을 아낀 모습을 볼 수 있다.


out-of-order issue & out-of-order completion

 issue 및 completion의 순서를 마음대로 변경할 수 있는 방법으로, 파이프라인을 최적화하기 가장 좋은 방법이다. 이 방법을 사용하기 위해서는 decode와 execution이 다른 스테이지로 명확하게 구분되어야 하며, 실행 순서를 지정하기 위해 decode한 instruction을 잠시 보관하기 위한 instruction window 하드웨어가 요구된다. 프로세서는 instruction들을 잠시 window에 넣은 후 가장 최적화 된 방식으로 instruction을 실행하여 사이클 수를 줄인다. 

instruction 실행 순서

위 그림에서는 issue 및 completion의 순서를 자유롭게 변경하여 실행 순서를 바꾸는 과정을 보여주고 있다. 이전 과정까지는 instruction의 실행 순서가 존재했기 때문에 I6은 I5보다 빨리 실행될 수 없었다. 그러나 현재 정책 하에서는 실행 순서까지도 변경될 수 있으므로 I6을 I4와 동시에 실행함으로써 필요한 사이클 수를 줄일 수 있었다.

 이때 dependency만 고려하면 I6, I4를 I1과 함께 실행되도록 올리고 I5와 I3을 동시에 실행되도록 구성하면 한 사이클이 더 줄어들 수 있다고 생각할 수 있다. 그러나 해당 동작을 위해서는 I4와 I6, I3과 I5가 함께 decode 되어야 한다. 이는 issue, completion가 아니라 fetch 수준에서 순서를 바꾸는 것에 해당하므로 현재 설명하고 있는 out-of-order issue & completion의 개념에 out-of-order fetch 같은 개념을 추가한 셈이 된다.

 기본적으로 superscalar는 runtime에 instruction을 fetch한 이후 해당 instruction에 대한 dependency을 고려하여 실행 순서를 정하는 dynamic issue을 채택하고 있으므로 사실 instruction의 fetch 순서 자체를 먼저 알 수 있기는 어렵다. 그럼에도 "가능"하다면 instruction fetch의 순서까지 변경하여 사이클 수를 최대한 줄이려는 노력에는 큰 의미가 있다.


Reorder Buffer

 out-of-order completion은 instruction이 데이터를 쓰는 순서를 변경할 수 있다. 예를 들어 아래 코드를 고려하자.

ADD R1,R2,R3
SUB R4,R5,R6

 일반적으로 생각해보면 ADD 명령은 반드시 SUB보다 선행되며, R1의 값은 R4보다 먼저 써져야 한다. 그러나 위에서 보인 것처럼 프로세서는 ILP(instruction level parallelism)을 높이기 위해 instruction의 실행 및 완료 순서를 자유자재로 변경할 수 있으므로 실제 실행 과정에서는 SUB가 ADD보다 어떤 방식으로든 선행될 수 있다.

 이러한 상황에서 SUB 명령이 먼저 수행된 후 ADD 연산 과정에서 오버플로우가 발생하여 overflow exception이 발생했다고 가정하자. 사용자는 디버그 과정에서 여러 레지스터 혹은 플래그의 값을 보며 해당 결과가 발생한 원인을 찾아야 한다. 이때 사용자는 명령이 코드 순서대로 실행되었다고 가정하고 디버깅을 수행하게 되는데, 실제로는 SUB가 먼저 실행되어 저장되었기 때문에 일부 레지스터 혹은 상태 정보에 있어서 순서와 다른 값이 저장되므로 사용자를 혼란스럽게 만들 수 있다. 이러한 경우를 imprecise exception이라고 한다. exception에 의해 얻는 정보가 임의 순서대로 진행된 명령에 의해 에러의 원인이나 값의 추적을 어렵게 만드는 것이다.

 imprecise exception을 방지하기 위해서는 특정 명령이 exception을 발생시키지 않음을 보장할 때까지 차후 실행되는 명령의 결과 값을 다른 곳에 저장해 둘 필요가 있다. 만약 exception이 발생하지 않는다면 이후의 값을 작성한다. 만약 exception이 발생하면 순차적 순서에서는 실행되지 않는 명령이므로 수행 결과 값을 날려버린다. 이를 통해 exception이 발생하더라도 실행 순서에 맞는 결과를 보장할 수 있게 된다.

 또 다른 이유로는 speculative execution의 rollback의 보장이 있다. speculative execution은 branch prediction 결과에 따라 실제 해당 branch가 실행될지 모르는 상황에서 미리 instruction을 실행해두는 방식으로, 확실하지 않은 미래를 도박하는 것 같은 동작때문에 speculative(투기의)이라는 표현이 붙었다. 만약 투기가 실패하는 경우 해당 instruction들은 실행되지 않은 것처럼 branch 관련 명령을 실행하기 이전으로 값들을 복구할 필요가 있다. reorder buffer이 존재하는 경우 speculation에 의한 결과 값을 잠시 보관한 후 해당 prediction이 맞을 때는 결과를 반영하고, 맞지 않을 때는 결과를 버림으로써 rollback을 수행할 수 있다.

요약하면 Reorder Buffer의 목적은 다음과 같다.

  • imprecise exception을 방지하기 위해 instruction의 결과 값을 잠시 저장하기 위해
  • speculative execution이 실패했을 때 rollback 동작을 지원하기 위해

Register Renaming

 data hazard 중 anti dependency(WAR)와 output dependency(WAW)는 instruction에서 사용되는 레지스터를 변경함으로써 해결할 수 있다. 이처럼 instruction 사이의 dependency(WAR, WAW)을 해소하기 위해 instruction 내에서 사용되는 레지스터의 이름을 변경하는 기법을 register renaming이라고 한다. 

R3 = R3 op R5
R4 = R3 + 1 
R3 = R5 + 1 (anti dependency)
R7 = R3 op R4 (true dependency)

output dependency 1,3 : 1이 실행되기 전에 3이 내용을 쓰면 안된다.
true dependency 1,2

anti dependency 2,3 : 2에서 R3을 읽기 전에 3이 실행되면 안된다.
true dependency 2,3,4

 위에서 보인 instruction들이다. 여기서 output dependency(1, 3) 및 anti dependency(2, 3)의 경우 register renaming을 통해 해소할 수 있다.

R3b = R3a op R5
R4 = R3b + 1 
R3c = R5 + 1 
R7 = R3c op R4 (true dependency)

전체적으로 R3의 이름을 R3a, R3b 및 R3c로 변경하여 레지스터에 대해 발생하는 data dependency을 없앤 모습이다. 기존에 존재하던 anti dependency 및 output dependency가 해소된 모습을 볼 수 있다.

 Register Renaming이 없는 경우 해당 시간만큼을 대기(stall)해야 한다. Scoreboarding과 같은 알고리즘이 여기에 속한다. 그러나 실행 환경에 충분한 레지스터가 존재하는 경우 dependency가 발생하는 레지스터들의 이름을 변경하여 대기로 인한 cycle 손해를 줄일 수 있다.

(좌) renaming을 수행하지 않는 경우와 (우) renaming을 수행하는 경우


Machine Parallelism

 위에서 언급한 대로 ILP가 제대로 동작하기 위해서는 머신 수준에서의 병렬성이 충분히 지원되어야 한다. Machine Parallelism은 하드웨어 리소스(장치, 파이프라인, functional unit 등)를 중복으로 가지는 것을 의미한다. 

  • out-of-order issue가 가능한 구조를 가져야 한다. 실행 순서를 바꾸면서 ILP을 높여야 하기 때문이다.(window 필요)
  • renaming이 반드시 보장되어야 한다. renaming이 보장되지 않는 경우 stall(대기)을 통해 data dependency을 해소해야 하는데, 이 경우 ILP가 크게 낮아지므로 machine parallelism이 큰 의미가 없게 된다.
  • reorder-buffer가 충분히 큰 크기로 존재하여 out-of-order completion이 가능해야 한다.

Branch Prediction

 branch는 conditional/procedural dependency을 발생시키며, 이는 다른 dependency에 비해 심각한 영향을 준다.

 RISC 머신에서는 dependency로 인한 ILP 하락을 막기 위해 (branch 실패하면 실행한 결과는 쓸모 없어지므로 차라리 branch 바로 뒤에 branch와 관계 없는 명령을 넣어서 실행하자는 논리) branch 명령 바로 뒤에 NOP을 삽입한 후 컴파일러를 이용하여 해당 위치에 branch와 독립적으로 동작하는 명령을 넣는 delayed branch라는 기법을 사용한다. 그런데 delayed branch에 의해 삽입되는 NOP의 개수가 많아질수록 이를 제거 가능한 확률이 크게 낮아지므로(1개는 80%, 2개 이상부터는 10% 이하라고 함), 파이프라인의 길이가 길어질수록 단순이 오버헤드로 작용하는 문제점이 있었다. 특히 현대 컴퓨터는 superscalar + superpipelined 방식을 채택하기 때문에 NOP을 제거하기 더욱 힘들어졌다.

 따라서 Superscalar 머신에서는 일반적으로 delayed branch가 그리 매력적인 선택지가 아니다. 대신 초기 RISC에서 사용하던 branch prediction 방식으로 복고하게 된다.

Conditional Instruction

 일반적인 머신에서는 branch 명령은 다른 동작들과 명확히 구분된 instruction으로 존재한다. 반면 ARM 아키텍처에서는 모든 instruction 내에 condition code가 존재하여 하나의 instruction 만으로도 branch + operation 동작이 구현된다.

일반적 머신에서의 코드가 ARM 아키텍처에서 대응되는 conditional instruction으로 나타나는 모습

 왼쪽의 코드에서는 BRANCH 명령과 MOV 명령이 명확하게 구분되어 있다. 이 둘 사이에는 control dependency가 존재하므로 ILP을 낮춘다. 오른쪽 코드의 경우 instruction 자체가 조건부로 실행될 수 있으므로 통상적인 코드에서 존재하던 control dependency 자체가 사라지며, instruction의 개수도 줄어든다. 파이프라인을 고려했을 때 instruction의 감소 + control dependency의 해소를 통해 ILP을 높일 수 있게 된다.

 Conditional Instruction은 ILP에 심각한 영향을 주는 control dependency을 덜 심각한 data dependency로 변경한다. 이를 통해 instruction의 의존성을 낮춰 파이프라인 구성 효율을 높인다.

superscalar의 동작을 개념적으로 묘사한 그림.