2009년 9월 26일 토요일

OpenSPARC Internals - 2

이번에는 OpenSPARC T2에 대해서 알아볼 차례입니다.

  • 전체적인 구성
T2의 전체적인 구성은 T1과 큰 차이를 보이지는 않습니다. 8 개의 core가 crossbar bus로 8개의 bank로 나누어진 L2 Cache에 연결된 형태를 가지고 있습니다. L2 Cache는 총 4MB이고 16-way set associavity를 가진다고 합니다. L2 cache의 2개의 bank 마다 하나씩 memory controller가 연결되는데 Fully Buffered DIMM을 사용하도록 변경하였습니다.


T1과 다른 점은 우선 공유 FPU가 없어지고, FPU는  각 Core에 들어가게 됩니다. 그리고 10 Gbps Ethernet MAC 2 port와 PCI Express가 내장된 형태를 가지고 있습니다.

  • Core 내부
T1에 비해 Core의 내부가 상당히 많이 변경되었습니다. T2은 Integer Execution Unit 2개와, Float Point Unit 1개, Load Store Unit 1개로 구성됩니다. 기본적으로 8개의 thread를 동시에 exeuction 할 수 있습니다. 8개의 thread를 2개의 group으로 나누어 4개의 thread 중에 하나를 골라 instruction을 issue합니다. 2개의 group이므로 2개의 instruction을 issue할 수 있는 구조를 가지고 있습니다. Integer Execution Unit이 2 개이므로 2개의 integer instruction 조합, 1개의 integer instruction+1개의 floating point insturction, 1 개의 integer instruction + 1 개의 load/store, 1개의 load/store + 1개의 floating point instruction을 issue할 수 있습니다. floating point unit에서 integer multiply와 divide를 처리하므로 그것은 좀 예외적으로 처리됩니다.

Integer pipeline도 8 stage로 변경되어  Fetch/Cache/Pick/Decode/Execute(ALU)/Memory/Bypass/Writeback 순서로 되어 있습니다. Pick Stage가 T1에 있었던 Switch stage에 해당하고, Bypass stage는 Load/Store Unit의 latency가 더 크므로 그것을 보상하기 위해서 들어 있다고 합니다.

16KB 8-way set associative Instruction cache를 가지고 있고, 8KB 4-way set associative write through data cache를 가지고 있습니다. 모두 physically tagged virtually indexed cache로 보입니다. 64 entry fully associative IITLB와 128 entry fully associative DTLB를 따로 가지고 있고, T1과는 다르게 hardware tablewalk를 지원하는 것으로 보입니다.

  • 동작 주파수 및 크기
UltraSPARC T2(코드명 Niagara II)로 발표되었을 당시(2007년)에 65 nm 공정을 사용하고 die size는 342mm^2로 알려졌습니다. 65 nm 공정으로 이동했고, L2 Cache의 크기가 큰 폭으로 증가하지 않았는데도 die area는 크게 줄지 않은 것으로 보아, FPU를 내장한 것이 큰 영향을 주었을 것으로 보입니다. 동작 주파수는 1.4 GHz입니다.

  • 의미
당연히 T1에 비해서 성능 향상이 이루어졌습니다. 우선 64 thread를 지원한다는 강점이 가장 크고, floating point 연산도 8개의 core에서 따로 수행할 수 있으므로 매우 빨라졌습니다. Clock frequency도 높아졌고, 다른 feature(L2 Cache의 size 등)도 좋아졌으므로 single thread performance도 어느 정도 향상되었다고 합니다. T2가 발표될 당시 SPEC 벤치마크에서 integer/floating point 연산 throughput에서 1등을 하였다고 하는데, 상당한 성능을 자랑했던 것으로 보입니다. 당연히 single thread의 performance는 다른 processor에 비해 느렸겠지만 throughput에서는 성능을 낼 수 있는 구조였으므로 가능했겠습니다.

Server에 집중할 거라면 굳이 FPU를 내장해서 Core 사이즈를 늘리지 말고, Core 갯수를 늘리거나 L2 Cache를 늘려서 integer 성능을 더 끌어올리는 방법이 어땠나 싶습니다. 어짜피 FLOPs로 판단하는 슈퍼컴퓨터 동네에서 IBM의 Power 시리즈를 누르기는 힘들었을 텐데 말이죠.

2009년 9월 16일 수요일

OpenSPARC Internals - 1

OpenSPARC에서 공개한 프로세서는 OpenSPARC T1과 OpenSPARC T2 두 종류입니다. 우선 OpenSPARC T1에 대해서 살펴 봅니다.

OpenSPARC T1은 기본적으로 8개의 64bit SPARC core가 내장되고 각 core 별로 4개의 thread를 동시에 실행할 수 있습니다. 따라서 총 32개의 thread(task)를 동시에 실행할 수 있는 구조를 가지고 있습니다.


  • 전체적인 구성

8개의 Core가 Unified Level 2 Cache를 공유하는 형태를 가집니다. L2 Cache는 4개의 bank로 나누어 관리됩니다. 각 Core가 동시에 L2 Cache에 data를 요청하는 경우를 줄이기 위해서 core가 요청하는 address에 따라서 L2 Cache의 4개의 bank 중에 하나가 선택되고 각 bank는 독립적으로 동작합니다. 각 Core와 L2 Cache의 4개의 bank는 on-chip crossbar bus로 연결됩니다. Level 2 Cache는 총 3MB의 12-way set associativity를 가진다고 합니다. DDR2 SDRAM Controller게 CPU에 직접 연결되는데, L2 Cache의 각 bank 마다 하나씩의 DDR2 SDRAM Controller가 연결됩니다.


재미 있는 것은 FPU(Floatingpoint Processing Unit)은 chip 전체에 하나만 있고, 8개의 core가 공유하여 사용합니다.


  • Core 내부

Core 내부는 정말 단순한 구조를 가집니다. 단, 하나의 integer pipeline만 존재하고 기본적으로 single issue입니다. pipeline의 구성은 Fetch/Switch(Thread Select)/Decode/Execute/Memory/Writeback의 6단계인데 Switch를 제외하면 전형적인 5 stage pipeline과 동일합니다(Load/Store Unit은 약간 더 긴 stage를 가진다고 합니다). Switch Stage가 바로 여러개의 thread를 돌릴 수 있게 해 주는 부분입니다. 이 부분의 동작 역시 아주 단순합니다. 4개의 thread에서 각각 읽어온 instruction 4개를 round-robin 형태로 하나씩 뽑아서 pipeline에 issue하는 형태를 가집니다. 단, 특정 thread가 cache miss 등으로 인해 pipeline stall이 발생하거나 latency가 긴 instruction(branch, multiply, load, divide)을 수행하는 경우에 해당 thread의 instruction issue를 멈추고 나머지 thread에서만 issue를 수행하다가 해당 thread의 pipeline stall 조건이 없어지면 다시 그 thread에서도 instruction issue를 수행합니다.


16KB의 Level 1 Instruction cache가 있는데 4개의 thread가 공유하고, 4-way set associative하고 32byte의 cache line size를 가집니다. 4개의 thread가 공유해야 하므로 physically indexed, physically tagged의 형태를 가집니다. 즉, virtual address가 아니라 physical address로 cache를 access한다는 말입니다.


8KB의 Level 1 Data cache를 가지고 있는데, 역시 4개의 thread가 공유하고 4-way set associative하고 16byte의 cache line size를 가집니다. L1 Instruction cache와 마찬가지로 physical address로 access하고 있는 것으로 보입니다. 기본적으로 write-through 형태를 가지고 별도로 각 thread별로 8개의 entry를 가지는 store buffer가 있어서 memory write를 buffering하게 됩니다.


당연히 Intruction TLB와 Data TLB를 각각 따로 사용하고 있고, 64 entry fully associative로 구성되고 4개의 thread가 공유하는 형태를 가집니다.


Branch instruction이 issue 되었을 때 해당 thread의 issue를 멈추는 것으로 보아 복잡한 branch predication은 수행하지 않는 것으로 보입니다.


4개의 thread를 처리할 수 있도록 추가된 하드웨어는 Swtich stage와, 각 thread별로 가지게 되는  PC, instruction buffer, register file, store buffer 정도가 됩니다. 물론 SPARC은 register window라고 하는 독특한 형태의 register file을 사용하므로, 각 thread별 register file의 추가가 어느 정도는 큰 hardware 크기 증가가 일어나기는 합니다만 단순히 multi-core를 통한 thread 늘리기에 비해서는 당연히 큰 증가가 아닐 겁니다.




  • 동작 주파수 및 크기
OpenSPARC T1에 대한 자료는 찾을 수 없고, UltraSPARC T1의 경우 90nm 공정에서 1.2GHz로 동작하는 것으로 알려져 있습니다. 그 때 size는 340 mm^2이다.



  • 의미


우선 T1의 경우 FPU를 8개의 core가 나누어 써야 하므로 scientific calculation이 많은 application에서는 성능이 제대로 나올 수 없습니다. 즉, IBM의 Power 시리즈 처럼 FLOPs로 성능이 측정되는 Super Computer 동네에서는 전혀 쓰일 가능성이 없습니다.


또한, 수행 시간, 즉 latency로 측정할 수 있는 단일 thread의 성능을 높이기 위해서 설계된 processor가 아닙니다. 우선 pipeline이 깊지 않아서 clock frequency가 높지 않습니다. core 하나가 4개의 thread를 round robin issue하는 형태이므로, core에서 4개의 thread가 동시에 동작한다면 1개의 thread를 돌릴 때에 비해 최악의 경우 1/4의 성능을 낼 수 밖에 없는 구조를 가집니다. 따라서 수행 시간이 중요한 system에서는 좋은 성능을 발휘할 수 없습니다. 예를 들어 chip 설계를 위한 EDA tool 등의 경우에도 좋은 성능을 기대할 수 없습니다.



4개의 thread를 round robin으로 issue하는 것이 가지는 의미는, 특정 thread가 cache miss와 같은 pipeline stall이 발생하였을 때에도, 다른 thread의 instruction이 issue가 일어날 수 있으므로 그로 인한 pipeline utilization 증가입니다.


Database와 같은 Server application의 경우에는 cache miss가 일반적인 다른 application에 비해 더 많이 발생한다고 합니다. 이런 cache miss가 많이 발생하고, latency 보다는 throughput이 중요한 system의 경우에서 의미를 가진다고 볼 수 있습니다. 수행 시간, FLOPs 가 의미를 가지는 것이 아니라 TPS(transaction per second)가 의미를 가지는 동네에서 성능을 발휘할 수 있겠습니다.


2009년 9월 6일 일요일

OpenSPARC Internals - 0

사실 OpenSPARC에 대해서 비교적 오래전부터 관심을 가지고 있었지만, 자료를 다운로드 받거나 하면 가입을 해야 하기에 좀 꺼려했습니다. 외국 사이트 가입은 그래도 이것 저것 많이 묻지 않아서 어느 정도 참을 만 하지만 워낙 그런 것들을 싫어하여 꼭 필요하지 않은 사이트면 가입을 하지 않는 결벽증이 있다고나 할까요? 사실 Source Code를 살펴보고 이렇게 하는 것은 아무래도 좀 시간이 걸리는 일이라서 시간적 여유가 있을 때까지는 미뤄둔다는 생각이 있었습니다. 그런데 얼마전에 다시 사이트에 방문해 보니 작년 말에 OpenSPARC Internals 이라는 책이 나왔는데, 국내에서 구하기도 어렵고, 사이트에 가입을 하면 PDF 형태로 책을 공개해 주어서 큰 맘 먹고 가입했습니다. 물론 PDF를 다운로드 받아서 훑어 보았습니다.

사실 OpenSPARC을 살펴 봐야 겠다고 생각한 것은 superscalar processor의 설계에 대해서 잘 모르고 있고, superscalar에 SMT까지 적용한 processor에 관심이 있었기 때문입니다. 하지만 재미 있는 것은 OpenSPARC은 superscalar processor가 아니었습니다.

OpenSPARC에 대해서 말하기에 앞서서 일반적인 processor 설계상 성능을 높이는 방법에 대해서 정리해 볼 필요가 있을 것 같습니다.


- clock frequency 높이기

대부분의 32bit/64bit CPU는 pipeline을 이용하여 이론적으로는 1 clock cycle에 1개의 instruction을 수행할 수 있도록 되어 있습니다. 성능을 높이는 가장 간단한 방법을 clock의 frequency를 높이는 방법입니다. 즉, 1 clock cycle에 처리하는 명령어는 1개로 동일하지만 1 clock cycle이 줄면, 즉 clock frequency가 높아지면 당연히 단위 시간당 처리하는 instruction의 갯수가 증가합니다.

clock frequency를 높이는 가장 단순한 방법은 사용하는 반도체 공정의 개선입니다. 물론 반도체 FAB을 운용하는 사람 입장에서는 FAB의 공정 개선이 상당히 어려운 문제입니다만 processor 설계하는 사람 입장에서는 그것은 "내 알 바 아니다."입니다. 반도체 공정이 0.13 um -> 90 nm -> 65 nm -> 45 nm까지 높아지면서 transistor의 동작 속도는 증가 하므로 clock frequency가 높아지게 됩니다.

Architecture, 정확하게는 micro architecture 상에서 clock frequency를 높이는 방법은 pipeline의 깊이를 깊게 가져가는 방법이 있습니다. 예를 들어 5 stage pipeline에서 7 stage pipeline으로 늘리게 되면 pipeline register(flipflop) 사이에 해야 하는 일, 즉 combinational path가 줄어들므로 clock frequency를 높일 수 있습니다. 하지만 이 경우에는 약간의 부작용이 생깁니다. 기본적으로 pipeline은 control hazard에 취약한데 그것이 pipeline이 깊어지면 더 문제가 되어 그것을 대응할 필요가 있습니다.
Control hazard는 branch instruction 수행에 관계된 문제입니다. Conditional branch(분기) instruction이 들어온 경우, 해당 branch가 조건이 true가 되어 분기가 일어날지, false가 되어 분기가 일어나지 않을지 결정하는데는 pipeline 상에 몇 clock cycle이 필요합니다. 그 사이까지 branch를 뒤따르는 instruction의 수행을 멈추거나 해야 하는데, 이것을 좀 완화하기 위해서 branch prediction(분기 예측)을 사용합니다. 가장 간단한 방법은 branch가 일어나지 않는다고 가정하고 뒤따르는 instruction을 계속 pipeline에 밀어넣은 후에, branch의 조건이 true가 되어 분기가 일어나야 하는 경우 pipeline상에 뒤따르던 instruction을 모두 무효화합니다. 이런 것을 pipline이 bubble로 채워진 상태라고 하는데 이런 것이 pipeline의 효율을 저하시킵니다. pipeline이 깊어지면 당연히 이런 비효율성이 늘어날 가능성이 크고 이를 위해서 좀더 정교한 branch prediction이 필요합니다. conditional branch의 경우 재미 있는 것은, branch가 실제로 분기할 가능성이 더 높다는 점입니다. 이것은 실행되는 branch의 상당수는 loop과 관계가 있기 때문입니다. 또 재미 있는 것은 conditional branch로 인해 분기가 일어났다면 다음에 그 branch instruction을 수행할 때 또 분기할 가능성이 높다는 점입니다. 이것도 역시 loop과 관계가 깊습니다. 따라서 branch가 과거에 분기 했느냐, 아니냐 등의 history를 저장하고 있는 table을 hardware가 가지고 있고 그 table에 따라 branch prediction을 하는 방법을 많이 사용합니다.

물론 pipeline 깊어지면 기타 data hazard를 회피하기 위해서 사용하는 forwarding 등의 방법도 마찬가지로 복잡해지거나 pipeline stall이 증가할 수 있습니다.

- ILP(Instruction Level Parallelism)

1 clock cycle에 하나의 instruction(operation)만 수행하라는 법은 없습니다. 1 clock cycle에 여러개의 operation을 동시에 수행하는 CPU를 만들어도 문제가 되지 않습니다. 이런 방법을 Instruction Level Parallelism이라고 부릅니다.
동시에 여러개의 operation을 수행할 수 있도록 CPU를 만들어야 하므로, operation을 수행하는 Execution Unit(ALU:Arithmetic Logic Unit 이나 Load Store Unit 등)을 여러개 넣어서 그것을 매 clock cycle 충분히 활용하다록 만드는 것입니다. 이런 접근 방법에는 크게 두 가지가 있습니다.

(a) VLIW(Very Long Instruction Word)
VLIW는 단순하게 생각했을 때 여러개의 operation을 하나의 긴 instruction에 넣고 일반적인 CPU가 하듯이 instruction을 매 clock cycle 실행할 수 있도록 하는 방법입니다. 예를 들어 다음과 같은 일반적인 CPU에서 사용하는 instruction sequence를 생각해 봅니다.
instruction 0 : add r0, r1, r2    ; r0 <= r1+r2
instruction 1 : sub r3, r4, r5    ; r3 <= r4+r5

VLIW CPU는 다음과 같은 instruction 하나를 이용할 수 있습니다.

instruction 0 : add r0, r1, r2 || sub r3, r4, r5

instruction 중간에 있는 "||"의 의미는 동시에 수행한다는 의미를 지니고, 위의 예는 instruction 하나에 addition과 subtraction을 동시에 처리하게 됩니다. 당연히 위의 경우에는 CPU 내부에 ALU가 두 개 있어야 합니다. 보통 Instruction을 execution unit에 넣는 일을 issue라고 표현하는데 위의 예제는 dual issue VLIW라고 표현할 수 있습니다. 동일한 clock cycle을 사용하고 매 clock cycle 마다 하나의 instruction을 issue할 수 있다고 하면 두 배의 성능이 날 수 있습니다.
위의 예제처럼 operation의 순서를 정렬하거나 순서를 바꾸는 일을 보통 instruction scheduling이라고 하고, VLIW의 경우 compiler가 compile할 때 scheduling을 통해 operation이 동시에 수행 가능하도록 instruction을 생성하게 됩니다.
문제는 이 instruction scheduling이 정말 어려워서 Compiler가 효율적으로 하지 못하고, 경우에 따라서는 scheduling이 전혀 안될 수도 있습니다. 예를 들어 위의 code sequence가 아래와 같이 조금만 변경되어도 data dependency로 인해 동시에 수행할 수 없습니다.

instruction 0 : add r0, r1, r2
instruction 1 : sub r3, r0, r4

add의 결과를 sub가 사용하기 때문에 동시에 수행할수 없습니다. 따라서 VLIW로 바꾸게 되어도 다음과 같이 변경해야 합니다.

instruction 0 : add r0, r1, r2 || nop
instruction 1 : sub r3, r0, r4 || nop

nop(no operation)이 추가된 instruction 두 개로 바뀌었습니다. 이 경우에는 성능 향상이 있을 수 없습니다.
Compiler를 만드는 동네에서는 Basic Block이라고 하여 operation sequence를 나누는 가장 작은 단위가 있습니다. Block의 끝은 branch이고, Block의 시작이 아닌 중간으로는 branch를 통한 진입이 되지 않는 가장 큰 operation sequence를 Basic Block이라고 합니다.즉 Basic Block 내부에서는 operation의 순서를 마음대로 바꾸어도 프로그램 동작에서는 전혀 이상이 없습니다. 문제는 대부분의 프로그램의 경우 compile을 할 때, 이 basic block 내부에 있는 operation의 갯수가 매우 작아서 instruction scheduling의 여지가 별로 없다는 것입니다.
이렇게 instruction scheduling이 잘 되지 않는 경우 nop(no operation)을 많이 사용할 수 밖에 없고, 그렇게 되면 code size가 증가하게 됩니다. code size가 증가한다는 것은 embedded system의 경우는 memory를 더 많이 요구한다는 것이고, 그 뿐 아니라 instruction cache의 효율성이 떨어진다는 것이므로 그로 인해 성능저하가 올 수 있습니다. 따라서 대부분의 경우는 nop이 적은 bit로 coding 될 수 있도록 instruction encoding을 하게 되는데 이 경우에는 또 instruction decoding 과정이 복잡해지는 단점이 생기게 됩니다.

또 VLIW로의 접근은 동일한 ISA(Instruction Set Architecture)를 기반으로 성능을 확장하는 것이 아닌 새로운 ISA를 정의하는 것입니다. 일반적으로 binary 하위 호환성이 있을 수 없습니다.
일반적인(?) CPU 시장에서 VLIW가 사용된 예가 있는데 그것이 바로 Intel과 HP가 공동으로 설계한 Itanium(IA-64, x86-64가 아님) 64bit Processor입니다. Intel과 HP는 VLIW라는 말을 사용하지 않고 EPIC(Explicit Parallel Instruction Computing)이라는 용어를 사용했는데 기본 개념은 비슷합니다. 90년대말 Itanium이 한창 개발될 때만 해도, Intel이 개발하는 첫번째 64bit processor로 성능이 매우 우수할 것으로 각광을 받으며 Server 업계를 평정할 것으로 예상했습니다만, 결과는 그다지 좋지 않았습니다. 역시 x86 binary 호환성이 문제가 되었다고들 합니다. 그사이 경쟁자들도 성능이 좋은 64bit CPU를 개발했고, x86 조차 AMD가 내놓은 Opteron으로 64bit에 진입했으므로 지금은 더욱 설자리가 줄었다고 해야겠습니다. 아직까지 명맥은 유지하고 있는 것으로 보아 적어도 특정 몇몇 분야에서는 성능이 괜찮은지도 모르겠습니다.
여러가지 이유로 인해 VLIW는 DSP(Digital Signal Processor)에 많이 사용됩니다. 일반적으로 팔리는 DSP 뿐 아니라, 여러 회사가 자체 VLIW CPU를 만들어 video processing 등에 사용하는 경우가 많습니다. 일반적으로 팔리는 VLIW DSP중에 대표적인 TI C64x+ 같은 경우 8개의 operation을 동시에 처리할 수 있는 8 issue VLIW processor입니다. DSP의 경우는 기본적으로 compiler를 많이 사용하지 않고 일명 손파일러, 즉 assembly coding을 주로 사용하게 됩니다. 적어도 성능이 많이 요구되는 core routine은 손파일러를 동원하게 됩니다. 손파일러를 사용하는 경우에는 여러개의 operation을 동시에 수행가능하도록 사람이 scheduling을 잘 하면 성능이 높게 나올 수 있습니다. 이 때는 보통 loop unroll이나 software pipeline 등의 방법을 이용하여 operation이 동시에 수행되는데 유리하도록 변경하는 과정을 거치게 됩니다. loop unroll 등은 compiler 등에서도 수행하고 있는데 숙련된 프로그래머의 손파일러가 역시 성능이 좋습니다.

(b) superscalar

VLIW가 instruction scheduling을 compile할 때 했다면 superscalar는 scheduling을 run time에 CPU가 직접하는 방법입니다. CPU가 다음에 수행할 하나의 instruction만 바라보고 있는 것이 아니라 여러개의 instruction을 큐에 담아서 살펴보고 있다가, 동시에 수행가능한 instruction을 찾아서 한꺼번에 issue하게 됩니다. 당연히 execution unit이 여러개 있고, 동시에 issue 될 수 있는 instruction의 갯수가 4 라면 4 way superscalar 라고 부릅니다.

기본적으로 in-order issue 방법이 있는데 이 경우에는 instruction의 순서에 맞추어 issue가 됩니다. 4 way superscalar의 경우라면 4 개의 instruction을 동시에 살펴봐서 앞에서 부터 문제가 없는한 많은 instruction을 동시에 issue하는 방법을 사용합니다. 순서대로 issue를 하게 되면 data dependency 등으로 인해, 여러개의 instruction을 동시에 issue할 수 없는 가능성이 커지게 됩니다.이것을 극복하기 위해서 out-of-order issue를 사용하기도 합니다.이 경우에는 issue할 수 있는 instruction의 갯수보다 더 많은 instruction을 살펴봐서 그 중에 dependency가 없는 것을 무조건 골라서 issue를 합니다. out-of-order issue의 경우에는 instruction의 순서대로 실행되지 않을 수 있습니다. 그런데 software 개발자 입장에서는 instruction이 순서대로 수행된다고 가정하고 프로그램을 만들었기 때문에 그것을 유지해 주어야 합니다. 그렇게 하기 위해서 pipeline의 끝에서 execution이 끝난 instruction이 적어도 프로그래머가 보기에 순서대로 수행되었다고 느낄 수 있게 끔 순서를 맞추는 과정이 필요합니다. 예를 들어 register 등의 update는 instruction의 순서에 맞추어 일어나도록 하는 과정이 있어야 프로그래머가 의도한 대로 프로그램이 수행된다고 느끼게 됩니다.

VLIW에 비해 superscalar가 가지는 장점은 우선 ISA의 변화가 없이 구현 가능하기 때문에 호환성을 유지해 줄 수 있다는 점입니다. 또한 성능적으로는 scheduling이 run-time이 일어나므로 schedule 되어 동시에 수행할 수 있는 가능성이 커집니다. VLIW는 basic block이라는 단위에서만 schedule을 할 수 밖에 없는데, superscalar의 경우 branch prediction 등을 통해 branch 후에 수행할 instruction 도 큐에 넣고 execution을 하는 방법 등을 통해 효율성을 높일 수 있습니다.

당연히 단점이 존재하는데, VLIW processor에 비해 hardware가 훨씬 복잡해 질 수 밖에 없습니다. CPU 내부에는 instruction que와 scheduling을 위해 instruction간 dependency를 detect할 수 있는 hardware가 존재해야 하므로 복잡도가 많이 증가합니다. 그러한 복잡도는 clock frequency를 높이는데 제약으로 작용할 가능성이 큽니다. 뿐만 아니라 out-of-order issue의 경우 exception 같은 event 상황을 대처하기가 어렵기 때문에 이것을 해결하기 위해서도 hardware가 상당히 더 들어가야 합니다.

Embedded processor로 많이 사용되는 ARM도 Coretex-A8이라는 core부터는 dual in-order issue superscalar 구조를 가지고 있습니다. embedded processor이므로 power budget이 desktop CPU나 server CPU에 비해 적으므로 상대적으로 간단하게 만들기 위해서 dual in-order issue 구조를 가지도록 하지 않았나 싶습니다.

참고로 superscalar processor에서도 프로그램을 compile할 때 compiler가 어느 정도의 instruction scheduling을 하게 됩니다. CPU가 동시에 수행하기 쉽도록 instruction scheduling을 해 주는데 당연히 VLIW 만큼 큰 제약을 가지지는 않습니다. 또한 VLIW processor용 compiler의 경우에는 speculation execution이라고 하여 basic block 단위를 넘는 instruction scheduling을 시도하는 경우도 있는 것으로 알고 있습니다.


- TLP(Thread Level Parallelism)

Pipeline을 더 깊게 만들어서 clock frequency를 아무리 높인다고 하더라도 pipeline의 data hazard나 control hazard 등으로 인해 효율성의 증가를 꾀하기 힘듭니다. 더욱 큰 문제는 cache miss로 인해 memory에서 data를 read하는데 걸리는 latency는 clock frequency가 증가하면 상대적으로 더 커지게 되는 문제가 있습니다. 또한 Clock frequency가 높아지면 그에 따른 power 소모가 증가하여 문제가 됩니다. Superscalar를 통한 ILP 접근 방법도 Instruction간의 dependency와 cache miss 등으로 인해 execution unit을 아무리 많이 넣어도 그에 따른 성능 향상을 기대하기 힘든 수준까지 도달한 것으로 보입니다. 이것을 극복하기 위해서 TLP(Thread Level Parallelism)이라는 별로 참신하지는 않지만 새로운 개념이 등장하였습니다.

대부분의 시스템이 multi-tasking OS를 기반으로 하고 있으므로 task를 동시에 여러개 돌릴 수 있도록 하는 것이 TLP의 핵심입니다. 당연히 단일 task(thread)가 동작하는데 걸리는 시간은 줄지 않습니다. 다만 여러개의 thread가 동시에 동작할 수 있으므로 전체적인 throughput이 향상이 됩니다. TLP도 역시 두 가지 approach가 있습니다.

(a) multi-core

정말 단순한 방법인데 CPU Core 여러개를 단일 chip에 넣는 것입니다. 서버에서는 CPU를 여러개 system에 넣어서 성능을 높이는 방법이 일반화 되어 있고, 반도체 공정이 미세화되면서 die area의 여유가 더 생겼기 때문에 가능한 일입니다. 제가 집에서 사용하는 PC도 AMD에서 만든 dual core CPU를 사용하고 있습니다. OS가 알아서 dual core에 적절히 task를 나누어 돌려 줍니다. 물론 dual core를 모두 활용할 필요가 없는 경우에는 성능 향상을 기대할 수 없습니다.

사실 단순히 CPU Core만 여러개 넣는다고 해서 multi-core가 되지 않습니다. Cache coherence 문제가 있기 때문입니다. CPU Core 마다 적어도 Level 1 Cache는 따로 가지는 것이 일반적입니다. 문제는 각 Core에 들어 있는 Cache에 동일한 address에 해당하는 memory 내용이 들어 있을 수 있는데, 이 내용이 Core 마다 다르면 커다란 문제가 생기게 됩니다. 특정 core가 memory write를 수행했는데 그것이 cache hit였고, 해당 memory 영역이 다른 core의 cache에 이미 존재하면 두 core 사이에 cache, 즉 memory의 내용이 달라지는 문제가 생깁니다. 이것을 해결하기 위해서 보통은 cache snooping이라는 방법을 사용해야 합니다. snoop이라는 영어 단어는 "기웃거리다"라는 뜻이라고 합니다. Cache snooping 방법은 다른 core의 cache update를 기웃거리며 살펴보고 그것을 통해서 cache를 스스로 update하는 방법입니다. 직접 구현해 본 바가 없어서 아주 구체적인 방법은 모르겠습니다.

(b) SMT(Simultaneous Multi-Threading)

SMT는 단일 CPU core에서 multi-threading을 구현하는 방법입니다. SMT는 크게 두 가지 문제를 해결하기 위해서 등장했습니다.

첫번째 memory latency 문제입니다. 기본적으로 CPU가 cache miss일 때 memory에서 data를 cache로 load해야 하는데 이 때 걸리는 시간이 매우 길다는 문제가 있습니다. 시스템의 main memory로 주로 사용되는 SDRAM(DDR, DDR1, DDR2 포함)은 latency가 매우 큰 memory입니다. SDRAM을 read하기 위해서는 row activation을 수행하고, RAS-to-CAS delay 후에 read command를 보내고 다시 CAS latency 만큼 기다려야 data가 나오게 됩니다. read 후에 다음의 read 사이에 또 precharge라는 것을 해야 하고 그것 또한 latency를 가지는데, 그것을 제외해도 상당한 latency를 가지고 있습니다. 예를 들어 삼성전자에서 나오는 일반적인 DDR2-800(pin당 800 Mbps 전송) SDRAM의 경우 RAS-to-CAS delay가 6 clock cycle(400MHz 기준), CAS Latency가 6 clock cycle(400MHz 기준) 정도입니다. 만약 CPU가 2GHz로 동작한다면 400MHz 기준 12 clock cycle이 2GHz에서는 60 clock cycle이 됩니다. 더욱 문제를 어렵게 하는 것은 SDRAM에서 data를 read/write하는 것이 CPU만이 아니라는 점입니다. 다른 peripheral이 SDRAM에 data를 read/write하고 있는 중이라면 훨씬 더 오래 걸립니다. CPU가 data를 요청했는데 cache miss 때문에 SDRAM에서 data를 읽어와야 한다면 그 사이 CPU는 더 이상 instruction을 수행하지 못하고 기다려야 하고 그에 따른 performance loss가 일어납니다.

Cache miss를 인해 CPU가 instruction을 수행할 수 없는 경우, thread를 바꾸어 다른 thread의 instruction을 그 기간 동안 처리한다면 CPU 성능의 낭비를 줄일 수 있습니다. 그런데 Cache miss가 났을 때, 그것을 OS에 알리고 OS가 thread(task) switching을 하게 한다면 배보다 배꼽이 더 크게 됩니다. OS가 task switching을 하기 위해서는 많은 instruction을 수행해야 하므로 cache miss를 처리하는 시간 보다 task switching을 하는데 걸리는 시간이 더 크기 때문입니다. 따라서 CPU 내부에 여러개의 thread context(PC, register, Virtual Address 관련 부분)을 모두 가지고 있다가 cache miss의 경우 hardware적으로 thread context를 전환하도록 해야 합니다.

두번째는 superscalar processor의 경우 instruction dependency 문제로 인해 instruction level parallelism을 충분히 활용할 수 없는 문제입니다.
아주 특수한 instruction을 제외하고는 서로 다른 thread의 instruction들 사이에는 dependency가 전혀 없습니다. 따라서 여러개의 thread에서 수행해야 하는 instruction을 동시에 보고 있으면 instruction dependency로 인해 issue를 못하게 될 확률이 줄어들게 됩니다. Intel은 이 방법을 활용하며 이름을 "Hyper Threading"이라고 하였습니다. 기억에 따르면 Pentium4 중 Code명 Northwood 부터 도입 되었는데, Core 시리즈(Core, Core2Duo, Core2Quad 등)에서 그 기능을 뺐다가 최근에 나온 i7부터 다시 도입되었습니다. Intel은 Core당 2개의 thread를 동시에 수행하는 구조를 가지고 있습니다. i7의 경우 Core가 4개이므로 총 8개의 thread를 동시에 실행할 수 있습니다.

SMT가 multi-core에 비해 지니는 장점은 하드웨어 추가가 적다는 점입니다. thread 갯수를 늘리기 위해서 multi-core는 thread 갯수에 정확하게 비례하는 hardware 추가가 필요하지만 SMT는 그렇지 않습니다. 하드웨어가 더 가져야 하는 것은 thread별로 context data(PC, General Purpose Register, Virtual Address 관련 부분 등) 정도만 있으면 되고, control logic이 약간 더 복잡해 지는 수준입니다. 나머지 부분, 즉 exeuction unit, cache 등은 thread 모두가 공유하는 구조를 가집니다. 대신 SMT를 통한 multi-thread는 multi-core 방법에 비해 성능 개선 폭이 작을 가능성이 큽니다. 즉, hardware 투입 대비 효율이 좋은 것이지 더 좋은 성능을 보증하지는 않습니다.

SMT와 multi-core는 서로 배제되는 방법이 아니기 때문에, Intel의 i7처럼 동시에 두 기술을 사용할 수도 있습니다.


SIMD(Single Instruction Multiple Data)를 빼 놓을 뻔 했습니다. SIMD를 통한 성능 향상은 위에서 언급한 방법과는 전혀 다릅니다. CPU에서 video나 audio 처리를 해야 하는 경우가 많아졌는데, 예를 들어 video 처리의 경우 8bit 혹은 16bit 단위의 data를 주로 사용합니다. 32bit CPU는 32bit Adder가 들어 있고 1 clock cycle에 32bit adding을 처리할 수 있습니다. 그런데 일반적인 프로그램에서 8bit(unsigned char) adding을 4번 하면, 32bit adder를 사용하더라도 4 clock cycle이 걸립니다. 재미 있는 것은 이미 32bit adder는 8bit adder 4개로 이루어져 있는 셈인데, 8bit adding 4번을 한번에 32bit adder로 처리할 수 있도록 하드웨어를 바꾸는 것은 매우 쉽습니다. 따라서 이렇게 하드웨어를 변경하고, 기존의 일반 add instruction 과는 다른 instruction을 추가하면 8bit adding 4번을 1 clock cycle에 처리할 수 있게 됩니다. 이런 방법을 사용한 대표적인 예는 Intel MMX/MMXEXT/SSE 등입니다. 예를 들어 Intel MMX의 경우 64 bit integer data를 하나의 instruction으로 처리합니다. 64bit integer data는 32bit integer 두개, 16bit integer 4개, 혹은 8bit integer 8개의 형태가 될 수 있습니다. x86을 제외한 다른 architecture에도 VIS니 Altivec이니 하는 SIMD instruction이 있습니다.

이번 글도 너무 길어져서 OpenSPARC에 관한 이야기는 다음으로 미루어야 겠습니다.