Search
Duplicate

컴퓨터 구조 및 설계/ 병렬 프로세서: 클라이언트에서 클라우드까지

서론

컴퓨터 설계자들은 오랫동안 컴퓨터 설계의 엘도라도를 찾으려고 애써왔는데, 그 꿈은 기존의 작은 컴퓨터 여러 개를 연결만 하면 강력한 성능의 컴퓨터가 되는 것이었다. 이러한 금빛 환상이 멀티프로세서(multiprocessor)의 근원이다.
(확실히 단일 기술은 소형화를 하고, 규모가 필요할 때는 그것들을 묶어서 사용하는 게 맞다)
만약 소프트웨어가 프로세서들을 효율ㅈ거으로 활용할 수 있다면 크고 비효율적인 프로세서를 작으면서 효율적인 프로세서 여러 개로 대치함으로써 단위 에너지당 성능을 개선할 수 있다. 이처럼 멀티프로세서는 확장 가능한(scalable) 성능을 제공할 뿐만 아니라 전력 효율도 개선할 수 있다.
멀티프로세서 소프트웨어에 확장성이 있으면 하드웨어가 일부 고장 나더라도 동작이 중지되지 않도록 설계할 수 있다. 즉 프로세서 n개 중에서 한 개가 고장 나면 남은 n-1개가 작업을 계쏙할 수 있게 하는 것이다. 이와 같이 멀티프로세서는 가용성을 개선할 수도 있다.
고성능이 여러 독립적인 작업들에 대한 높은 처리량을 의미하는 경우도 있다. 이를 태스크 수준 병렬성(task-level parallelism) 혹은 프로세스 수준 병렬성(process-level parallelism)이라고 한다.
이때 병렬로 수행되는 작업 하나 하나는 독립적인 응용 프로그램으로 이런 방식은 흔하면서도 중요한 병렬 컴퓨터 사용 방법 중 하나이다. 이는 여러 개의 프로세서가 한 작업을 나누어서 수행하는 것과는 대조된다.
이를 구별하기 위해 한 개의 프로그램이 여러 프로세서에서 동시에 수행될 때 병렬처리 프로그램(parallel processing program)이라는 용어를 사용한다.
항상 더 빠른 컴퓨터를 필요로 하는 과학 계산 문제들이 있었다. 이런 문제들 때문에 지난 수십 년 동안 여러 독창적인 병렬 컴퓨터들이 등장했다. 어떤 문제들은 독립적으로 동작하는 다수의 서버들로 구성된 클러스터를 사용하여 간단히 해결될 수 도 있다.
클러스터(cluster)는 과학 분야 뿐 아니라 검색엔진, 웹 서버, 전자우편 서버, 데이터베이스 같이 부하가 많은 응용에도 사용될 수 있다.
1장에서 설명한 바와 같이 멀티프로세서가 다시 큰 주목을 받게 된 것은 명시적으로 하드웨어 병렬성을 이용하여 성능을 올리는 것이 클럭 주파수를 올리거나 CPI를 올리는 것보다 에너지 측면에서 더 유리하기 때문이다.
같은 이름에서 중복을 피하기 위해 이런 프로세서를 '멀티프로세서 마이크로프로세서' 대신 '멀티코어 마이크로프로세서(multicore microprocessor'라고 부른다.
멀티코어 칩에서는 프로세서를 코어(core)라고 부른다. 코어의 수는 Moore의 법칙에 따라 배가될 것으로 예측되고 있다. 멀티코어는 대부분 실제 주소공간을 공유하는 공유 메모리 프로세서(SMP; shared memory processor)이다.
오늘날 성능을 중시하는 프로그래머는 병렬 프로그래머가 될 수 밖에 없다. 왜냐하면 이제 순차 프로그램은 느린 프로그램을 의미하기 때문이다.
산업체가 직면한 큰 문제는 칩 안에 집적되는 코어의 수가 늘어남에 따라 성능과 에너지를 효율적으로 활용하는 벼렬 프로그램을 정확하고 쉽게 작성하도록 하는 하드웨어와 소프트웨어를 발명하는 것이다.
마이크로프로세서 설계에서 변화가 급격하게 이루어지다보니 신중하게 대처할 여유가 없었고, 이 때문에 용어의 사용과 그 의미에서 심각한 혼란이 일어나고 있다.
그림 6.1에서 직렬적(serial), 병렬적(parallel), 순차적(squential), 병행적(concurrent)이라는 용어들을 구별하려고 시도해 보았다.
이 그림에서 열은 소프트웨어를 나타내는데, 소프트웨어는 근본적으로 순차적이든지, 병행적이다. 행은 하드웨어를 나타내는데 하드웨어는 직렬적이거나 병렬적이다.
예컨대 컴파일러를 만드는 프로그래머는 컴파일러를 파싱, 코드 생성, 최적화 등이 단계적으로 진행되는 순차적인 프로그램으로 생각한다.
반면 운영체제를 만드는 프로그래머는 운영체제를 컴퓨터에서 실행 중인 독립적 작업들의 입출력 이벤트를 처리하는 프로세스들로 구성된 병행적인 프로그램으로 생각한다.
그림 6.1에서 하드웨어와 소프트웨어를 다른 축으로 표시한 이유는 병행 소프트웨어가 직렬 하드웨어에서 수행될 수도 있고, 병렬 하드웨어에서 수행될 수도 있음을 보여주기 위해서이다. 순차 소프트웨어에 대해서도 마찬가지다.
자연스럽게 순차적으로 작성된 프로그램을 어떻게 병렬 하드웨어에서 빠르게 실행할 수 있으냐가 병렬 혁명에서 해결해야 할 유일한 문제라고 생각할 수도 있겠다. 그러나 프로세서 수가 증가함에 따라 병행 소프트웨어의 성능이 계속해서 좋아지게 하는 것도 쉽지 않으며, 이 또한 해결해야 할 문제 중 하나이다.
앞으로는 순차적이거나 병행적이거나 병렬 하드웨어에서 수행되는 소프트웨어는 병렬처리 프로그램 혹은 병렬 소프트웨어라고 부르도록 한다.

병렬처리 프로그램 개발의 어려움

병렬처리의 어려움은 하드웨어에 있지 않다. 문제는 멀티프로세서에서 더 빨리 수행하도록 재작성된 응용 프로그램들의 수가 너무 적다는 것이다. 멀티프로세서를 사용하여 한 개의 태스크를 더 빨리 수행하는 소프트웨어를 작성하는 것은 어려운 일이다. 프로세서의 개수가 증가하면 문제는 더 심각해 진다.
왜 순차적인 프로그램을 개발하는 것보다 병렬처리 프로그램을 개발하는 것이 더 힘든가? 첫째 이유는 기왕 병렬 프로그램을 사용할 바에는 더 높은 성능과 에너지 효율을 얻을 수 있어야 하기 때문이다.
그렇지 못하다면 단일 프로세서에서 프로그래밍 하기 쉬운 순차 프로그램을 사용하는 편이 낫다. 실제로 프로그래머의 개입이 전혀 없이도 명령어 수준의 병렬성(ILP: instruction-level parallelism)을 활용하는 수퍼스칼라나 비순차 실행 같은 단일 프로세서 설계 기술이 이미 개발되어 사용되고 있다. (4장 참조)
이런 혁신적인 기법 때문에 멀티프로세서를 위해 프로그램을 다시 작성하는 것에 대한 요구가 크지 않았다. 왜냐하면 프로그래머가 아무 일도 하지 않아도 순차적인 프로그램이 새로운 컴퓨터에서 더 빠르게 돌아갈 수 있었기 때문이다.
빠르게 수행되는 병렬처리 프로그램을 작성하기 힘든 이유가 무엇인가? 특히 프로세서 개수가 증가할수록 어려워지는 이유는 무엇일까?
1장에서 일을 8배 빠르게 할 목적으로 8명의 기자가 동시에 하나의 기사를 쓰는 비유를 사용한 바 있다. 이 방법이 성공하려면 한 작업이 동일한 크기의 8개 부분으로 나누어져야 한다. 크기가 동일 하지 않다면 큰 부분을 담당하는 사람이 일을 끝낼 때까지 나머지 사람들이 기다려야 할 것이기 때문이다.
성능을 저하시킬 수 있는 또 다른 위험은 기자들이 자기가 맡은 기사 작성보다 다른 사람들과 의견을 교환하는데 더 많은 시간을 보낼 수 있다는 것이다.
이 비유와 같이 병렬 프로그래밍에서 우리가 부닥치는 문제에는 작업의 스케쥴링, 작업을 병렬 조각으로 나누는 일, 부하를 균등하게 하는 일, 동기화 시간을 줄이는 일, 그리고 서로 통신하는 오버헤드를 줄이는 일 등이 있다.
기사 하나를 만들기 위해서 더 많은 기자가 참여할수록 마찬가지로 병렬 프로그래밍을 위해 더 많은 프로세서를 사용할 수록 이런 문제는 더욱 어려워진다.
1장에서 우리는 또 하나의 장애 요인인 Amdahl의 법칙에 대해 살펴보았다. 이 법칙은 프로그램이 여러 프로세서를 효율적으로 사용하기 위해서는 프로그램의 작은 부분까지도 모두 병렬화되어야 한다는 사실을 환기시킨다.

예제) 속도 개선의 어려움

100개의 프로세서를 가지고 90배의 속도 개선을 얻기를 원한다고 하자. 원 프로그램에서 최대 몇 %까지는 순차적으로 실행되도 좋을까?
1장의 Amdahl의 법칙은 다음과 같다.
개선 후 실행시간 = (개선에 의해 영향을 받는 실행시간 / 개선의 크기) + 영향을 받지 않는 실행시간
이 Amdahl의 법칙을 속도 개선과 원 수행시간 간의 관계식으로 다시 쓰면 다음과 같다.
속도 개선 = 원 수행 시간 / ( (원 수행시간 - 개선될 부분의 시간) + (개선될 부분의 시간 / 개선의 크기) )
원 수행시간이 1이라고 가정하고 이 식을 다시 정리하면 개선될 부분의 시간이 원 수행시간에 대한 비율로 바뀐다.
속도 개선 = 1 / ( (1 - 개선될 부분의 비율) + (개선될 부분의 비율 / 개선의 크기) )
기대하는 속도 개선 90을 식에 대입하고 개선의 크기에 100을 대입하면 다음과 같다.
90 = 1 / ( (1 - 개선될 부분의 비율) + (개선될 부분의 비율 / 100) )
이 식을 풀어서 개선될 부분의 비율을 구하면 다음과 같다.
90 x (1 - 0.99 x 개선될 부분의 비율) = 1
90 - (90 x 0.99 x 개선될 부분의 비율) = 1
90 - 1 = 90 x 0.99 x 개선될 부분의 비율
개선될 부분의 비율 = 89 / 89.1 = 0.999
따라서 100개의 프로세서를 이용하여 90배의 속도 개선을 얻기 위해서는 전체 프로그램에서 순차적인 부분의 비율이 0.1% 이하가 되어야 한다.

예제) 속도 개선의 어려움: 큰 문제

다음 두 가지 덧셈을 수행한다고 하자. 하나는 10개의 스칼라 변수의 합을 구하는 것이고 다른 하나는 10x10 2차원 행렬의 합을 구하는 것이다. 지금은 행렬의 합만이 병렬화될 수 있다고 가정하자. 스칼라 합을 어떻게 병렬화하는지는 곧 보게 될 것이다.
10개의 프로세서를 사용하는 경우와 40개의 프로세서를 사용하는 경우 속도 개선을 얼마나 얻을 수 있을까? 또 행렬의 크기를 20x20으로 늘렸을 때의 속도 개선도 구하라.
덧셈 연산을 수행하는 시간을 t라고 하고 성능을 t의 함수로 가정하자. 이때 스칼라 변수 10개의 덧셈은 병렬 프로세서를 사용해도 이득을 얻지 못하지만, 100번의 덧셈은 병렬 프로세서를 사용해서 이득을 얻을 수 있다. 단일 프로세서에서의 수행시간은 110t이고 그 10개의 프로세서를 사용하는 경우의 수행시간은 다음과 같다.
개선 후 실행시간 = (개선에 의해 영향을 받는 실행시간 / 개선의 크기) + 영향을 받지 않는 실행시간
개선 후 실행시간 = (100t / 10) + 10t = 20t
따라서 10개의 프로세서를 사용하는 경우의 속도 개선은 110t/20t = 5.5이다. 같은 식으로 40개의 프로세서를 사용하는 경우의 수행 시간을 구하면 (생략) 8.8이 된다.
이 크기의 문제에 프로세서 10개를 사용하면 이상적인 경우의 55%에 해당되는 만큼의 속도 개선을 얻는 반면, 40개를 사용하면 이상적인 경우의 22%에 해당되는 만큼만 속도 개선을 얻을 뿐이다.
행렬의 크기를 키우면 어떻게 되는지 알아보자. 순차적으로 수행하는 경우의 수행시간은 10t + 400t = 410t이다. 이 프로세서 10개를 사용하는 경우의 수행시간은 다음과 같으며
개선 후 수행시간 = (400t / 10) + 10t = 50t
따라서 속도 개선은 410t/50t = 8.2이다. 프로세서를 40개 사용하는 경우는 (생략) 20.5가 된다. 이렇게 큰 프로그램의 경우 프로세서 10개를 사용할 때에는 이상적인 경우의 82%, 40개를 사용할 때는 이상적인 경우의 51%의 속도 개선을 얻을 수 있다.
이 예제를 통하여 알 수 있는 바는 문제의 크기를 고정시킨 상태에서 속도 개선을 얻는 것이 문제의 크기를 증가시켜서 속도 개선을 얻는 것보다 어렵다는 것이다. 그 차이를 구별하기 위해 스케일링 방법을 표시하는데 두 가지 용어를 사용한다.
경성 스케일링(strong scaling)은 문제의 크기를 고정시킨 상태에서 얻어지는 속도 개선이고, 연상 스케일링(weak scaling)은 프로세서의 수에 비례하여 문제 크기를 증가시키는 경우의 속도 개선이다.
문제의 크기 M이 메인 메모리 내의 워킹셋(working set)과 같고 프로세서는 P개가 있다고 가정하자. 경성 스케일링의 경우 프로세서당 메모리는 대략 M/P이고 연성 스케일링의 경우는 대략 M이다.
경성 스케일링보다 연성 스케일링이 더 쉽다는 것이 일반적인 통념이지만 메모리 계층구조 때문에 반대 현상이 생길 수도 있다.
예컨대 연성 스케일링의 문제가 너무 커져서 멀티코어 멀티프로세서의 마지막 단계 캐시에 다 들어가지 않으면 경성 스케일링을 사용하는 경우보다 성능이 훨씬 더 나빠질 수 있다.
응용에 따라서 둘 중 한 가지 스케일링 방식을 선택할 수 있다. TPC-C 차변-대변(debit-credit) 데이터베이스 벤치마크에서 분당 트랜잭션의 성능이 높아지면 고객의 계좌 수를 늘린다.
이는 은행이 빠른 컴퓨터를 장만했다고 해서 한 고객이 갑자기 하루에 100배 이상의 ATM 작업을 수행하기 시작했다고 생각하는 것은 사리에 맞지 않기 때문이다.
그것보다는 고객 수를 100배로 늘린 상태로 실험해서 시스템이 100배 더 많은 트랜잭션을 처리할 수 있다는 것을 보이는 것이 타당할 것이다. 큰 문제는 종종 많은 양의 데이터를 필요로 하며 이것이 연성 스케일링의 타당한 근거가 된다.
다음 예제는 부하균형(load balancing)의 중요성을 보여준다.

예제) 속도 개선의 어려움 부하균형

앞의 예제에서 40개의 프로세서를 사용하여 20.5배의 속도 개선을 얻기 위해서 우리는 부하가 완전히 균형을 이룬 것으로 가정하였다. 즉 각 프로세서가 2.5%씩 부하를 나누어 가졌다고 생각하였다. 이 예제에서는 어떤 프로세서의 부하가 나머지 프로세서들보다 더 크면 속도 개선에 어떤 영향을 미치는지를 살펴 보고자 한다.
이를 위해 한 프로세서의 부하가 두 배 더 증가하는 경우(5%)와 다섯 배 더 증가하는 경우(12.5%)의 속도 개선을 계산하라. 나머지 프로세서들은 얼마나 잘 활용되고 있는가?
한 프로세서가 병렬로 수행될 부분의 5%를 담당하면 5% x 400, 즉 20개의 덧셈을 수행하고 나머지 39개의 프로세서가 380개의 덧셈을 나누어 수행하게 된다. 이 연산들이 모두 동시에 수행되므로 수행시간이 가장 많이 걸리는 경우에만 계산하면 된다.
개선 후 수행시간 = Max(380t/39, 20t/1) + 10t = 30t
속도 개선이 20.5에서 410t/30t = 14배로 감소하였다. 한 프로세서가 20t 동안 쉬지 않고 일을 수행하는 동안 나머지 39개의 프로세서는 380t/39 = 9.7t 시간만 일하고 나머지 반 이상의 시간을 놀게 된다.
만약 한 프로세서가 12.5%의 부하를 갖는다면 이 프로세서는 50개의 덧셈을 수행하여야 한다.
개선 수 수행시간 = Max(350t/39, 50t/1) + 10t = 60t
속도 개선은 더 떨어져서 410t/60t = 7이 된다. 나머지 프로세서는 20% 이하(9t/50t)만 활용된다. 이 예제는 부하균형의 가치를 증명한다. 한 프로세서의 부하가 다른 프로세서들보다 2배 커지면 성능은 1/3만큼 떨어지고, 5배 커지면 성능은 3배 가까이 떨어진다.

SISD, MIMD, SIMD, SPMD와 벡터

1960년대 제안된 병렬 하드웨어 분류 방법이 있는데 이것은 오늘날에도 여전히 사용되고 있다. 이 분류는 명령어 스트림의 개수와 데이터 스트림의 개수에 기반을 두고 있다. 그림 6.2는 이 분류법을 보여준다.
전통적인 단일 프로세서는 한 개의 명령어 스트림과 한 개의 데이터 스트림을 가지며, 전통적인 멀티프로세서는 여러 개의 명령어 스트림과 여러 개의 데이터 스트림을 갖는다. 이 두 가지를 각각 SISD와 MIMD라 부른다.
MIMD 컴퓨터에서는 각 프로세서가 서로 다른 프로그램을 수행하면서도 모든 프로그램이 협력하여 크고 통합된 목적을 달성하도록 프로그래밍하는 것이 가능하다.
하지만 보통은 모든 프로세서가 같은 프로그램을 수행하되, 조건문을 사용하여 각 프로세서가 코드의 서로 다른 부분을 수행하도록 하는 방법을 사용한다. 이런 방식을 SPMD(Single Program Multiple Data)라고 하는데, 새로운 범주는 아니고 MIMD 컴퓨터를 프로그래밍하는 보통 방법일 뿐이다.
여러 개의 명령어 스트림을 가지면서 단일 데이터 스트림을 갖는 MISD로 분류될 수 있는 컴퓨터와 가장 유사한 것은 단일 데이터 스트림에 대하여 파이프라인 방식으로 일련의 계산을 수행하는 '스트림 프로세서(stream processor)'일 것이다.
예컨대 네트워크로 받은 입력을 구문 분석하고, 암호를 풀고, 압축을 푼 다음, 매칭 연산을 수행하는 등의 일련의 작업을 파이프라인 방식으로 수행하는 것이다.
MISD의 반대인 SIMD의 예는 많이 있다. SIMD 컴퓨터는 벡터 데이터에 대한 연산에 유용하게 사용된다.
예컨대 SIMD 명령어 하나는 64개의 데이터 스트림을 64개의 ALU로 보내서 한 클럭 사이클에 64개의 합을 계산할 수 있다. 3.6절과 3.7절에서 살펴본 서브워드 병렬 명령어는 SIMD의 한 예다. 실제로 Intel SSE 약어의 두 번째 글자가 SIMD를 의미한다.
SIMD의 장점은 병렬 실행 유닛들이 모두 동기화되어 있어서 한 프로그램 카운터(PC)로 인출한 명령어 하나를 일제히 수행한다는 것이다.
프로그래머의 관점에서 보면 이것은 이미 익숙한 SISD와 거의 비슷하다. 모든 유닛들이 동일한 명령어를 수행하는 것은 같은데, 실행 유닛마다 별도의 주소 레지스터를 가지고 있어서 다른 데이터 주소를 사용할 수 있다는 점이 다를 뿐이다.
6.1절에서 소개한 용어를 사용하여 설명한다면, 순차적인 프로그램을 직렬 하드웨어인 SISD에서 실행되도록 컴파일할 수 있도 있고, 병렬 하드웨어인 SIMD에서 실행되도록 컴파일할 수도 있다.
원래 SIMD를 개발하게 된 동기는 여러 실행 유닛들을 제어하는 제어 유닛을 하나로 통합해서 비용을 절약하자는 것이었다. 그 외에 프로그램 메모리의 크기가 작아도 된다는 장점이 있다.
메시지 전달 MIMD는 프로세서마다 별도의 프로그램을 가지고 있어야 하고, 공유 메모리 MIMD는 여러 개의 명령어 캐시를 가지고 있어야 하지만 SIMD는 동시에 수행할 프로그램 하나만 있으면 된다.
SIMD는 for 순환문에서 배열을 처리할 때에 가장 효율적이다. 그러므로 SIMD의 병렬성을 잘 활용하려면 동일한 구조를 갖는 데이터가 매우 많이 있어야 하는데, 이를 데이터 수준 병렬성(data-level parallelism)이라고 부른다.
SIMD는 case나 switch 문장을 실행할 때 약점을 보인다. 이때는 각 실행 유닛이 자신이 가진 데이터가 무엇인가에 따라 다른 동작을 수행해야 하기 때문이다.
부적합한 데이터를 가진 실행 유닛들은 동작을 억제시키고 적합한 데이터를 가진 유닛들만 동작하도록 해야 한다. 이때 case나 switch의 경우의 수가 n이라면 성능은 1/n로 떨어진다.
SIMD의 효시라고 할 수 있는 소위 어레이 프로세서들은 이미 역사 속으로 사라져갔다. 그러나 두 종류의 SIMD는 여전히 널리 사용되고 있다.

x86의 SIMD: 멀티미디어 확장 명령어

5장에서 설명한 바와 같이 크기가 작은 정수형 데이터에 대한 서브워드 병렬성은 1996년에 소개된 x86 마이크로프로세서의 멀티미디어 확장 명령어 MMX가 나온 동기가 되었다.
Moore의 법칙대로 더 많은 명령어들이 추가되어 스트리밍 SIMD 확장(SSE; Streaming SIMD Extension)으로 최근에는 개선된 벡터 확장(AVX: Advanced Vector Extension)으로 발전되었다.
AVX는 64비트 부동소수점 연산 4개를 동시에 처리할 수 있다. 연산과 레지스터의 데이터 크기는 멀티미디어 명령어의 opcode에 표시된다. 레지스터와 연산의 데이터 크기가 커지면서 멀티미디어 명령어 개수도 폭발적으로 증가하여 현재는 수백 개의 SSE 명령어와 AVX 명령어들이 제공되고 있다.

벡터

더 오래됐지만 더 우아한 SIMD로 벡터 구조가 있는데, 사람들은 이것을 1970년대에 Seymour Cray가 설계한 컴퓨터와 동일시해 왔다. 이 구조는 데이터 수준 병렬성을 많이 갖고 있는 문제에 딱 맞는다.
예전의 어레이 프로세서가 64개의 ALU로 동시에 64개의 덧셈을 수행하던 것과는 달리, 벡터 구조는 ALU를 파이프라인으로 구성하여 낮은 비용으로 좋은 성능을 얻는다.
벡터 구조의 기본 원리는 메모리에서 데이터 원소들을 꺼내서 큰 레지스터 집합에 넣은 다음 레지스터에 있는 데이터에 대하여 파이프라인 실행 유닛을 사용하여 순차적으로 연산하고 그 결과를 다시 메모리에 저장하는 것이다.
벡터 구조의 핵심적인 특징은 벡터 레지스터 집합이다. 예컨대 64비트 우너소 64개를 저장할 수 있는 벡터 레지스터 32개를 갖는 것이다.

예제) 벡터 코드와 일반 코드와의 비교

MIPS 명령어 집합 구조에 벡터 명령어와 벡터 레지스터를 추가한다고 가정하자. 벡터 연산은 MIPS 연산과 같은 이름을 사용하되 끝에 'v'자를 덧붙인다. 예컨대 addv.d는 2배 정밀도 벡터 두 개를 더하는 명령이다.
벡터 명령어의 입력은 벡터 레지스터 두 개가 될 수도 있고(addv.d), 벡터 레지스터 하나와 스칼라 레지스터 하나가 될 수도 있다(addvs.d). 후자의 경우 스칼라 레지스터의 값은 모든 연ㅅ나에 다 사용된다.
예컨대 addvs.d는 벡터 레지스터의 모든 원소에 스칼라 레지스터의 값을 더한다. lv, sv는 load vector, store vector의 약자로 2배 정밀도 벡터 데이터 전체를 적재하거나 저장하는 명령어이다. 한 피연산자는 적재될 혹은 저장할 벡터 레지스터이고, 다른 피연산자는 MIPS의 범용 레지스터로서 벡터가 저장된(또는 저장될) 메모리의 시작 주소를 가지고 있다.
이상의 짧은 설명을 바탕으로 다음 연산에 해당하는 일반적인 MIPS 코드와 벡터 MIPS 코드를 보여라.
Y=a×X+YY = a \times X + Y
여기에서 a는 2배 정밀도의 스칼라 변수이고, X와 Y는 64개의 2배 정밀도 부동소수점 숫자의 벡터로서 메모리에 있다. (이 예제는 DAXPY 순환문이라고 불리는 Linpack 벤치마크의 내부 순환문이다. DAXPY는 'double precision a x X plus Y'에서 따온 말이다)
DAXPY의 일반 MIPS 코드는 다음과 같다.
l.d $f0, a($sp) :load scalar a addiu $t0, $s, #512 :upper bound of what to load loop: l.d $f2, 0($s0) :load x(i) mul.d $f2, $f2, $f0 :a x x(i) l.d $f4, 0($s1) :load y(i) add.d $f4, $f4, $f2 :a x x(i) + y(i) s.d $f4, 0($s1) :store into y(i) addiu $s0, $s0, #8 :increment index to x addiu $s1, $s1, #8 :increment index to y subu $t1, $t0, $s0 :compute bound bne $t1, $zero, loop :check if done
C++
DAXPY의 벡터 MIPS 코드는 다음과 같다.
l.d $f0, a($sp) :load scalar a lv $v1, 0($s0) :load vector x mulvs.d $v2, $v1, $f0 :vector-scalar multiply lv $v3, 0($s1) :load vector y addv.d $v4, $v2, $v3 :add y to product sv $v4, 0($s1) :store the result
C++
이 예제의 두 코드 사이에 재미있는 비교할 거리가 있다. 가장 극적인 것은 벡터 프로세서가 동적 명령어 대역폭을 획기적으로 줄였다는 것이다. MIPS에서 거의 600개나 달하던 명령어를 6개로 크게 줄였다.
이렇게 줄일 수 있었던 것은 벡터 연산 하나가 64개의 원소를 처리하기 때문이고, MIPS에서 거의 반 정도를 차지하던 오버헤드 명령어들이 벡터 코드에는 하나도 없기 때문이다. 예상할 수 있듯이 인출되는 명령어가 적어지면 에너지 소모도 줄어든다.
또 다른 중요한 차이는 파이프라인 해저드의 빈도에 관한 것이다(4장 참조). 일반 MIPS 코드에서 add.d 명령어는 mul.d 명령어가 끝날 때까지 매번 기다려야 하고, s.d 명령어는 add.d 명령어가 끝날 때까지 매번 기다려야 하며, add.d 명령어와 mul.d 명령어는 l.d 명령어를 기다려야 한다.
벡터 프로세서에서 벡터 명령어들은 각 벡터의 첫 번째 원소에 대해서만 지연되고 나머지 원소들은 파이프라인을 따라서 거침없이 흘러갈 수 있다. 따라서 매 원소 연산마다 파이프라인이 지연되는 대신에 벡터 연ㅅ나 하나당 한 번씩만 지연된다.
이 예제에서 MIPS의 파이프라인 지연 발생 빈도는 벡터 버전의 MIPS 보다 64배 높다. MIPS의 파이프라인 지연은 순환문 펼치기를 사용하여 줄일 수 있다(4장 참조) 그러나 명령어 대역폭의 차이는 줄일 수 없다.
AVX 명령어에서 서브워드 병렬성과 같이 벡터의 각 원소는 서로 독립적이기 때문에 병렬로 연산을 수행할 수 있다. 오늘날 모든 벡터 컴퓨터는 여러 줄의 벡터 파이프라인(벡터 레인이라고 한다. 그림 6.3과 6.4 참조)을 갖는 벡터 기능 유닛이 있어서 클럭 사이클당 2개 이상의 연산 결과를 생성한다.

벡터와 스칼라의 비교

벡터 명령어는 일반적인 명령어 집합 구조(스칼라 구조라고 불리는)와 비교할 때 몇 가지 중요한 특징이 있다.
벡터 명령어는 많은 양의 작업을 지정하므로, 벡터 명령어 하나가 순환문 전체 수행과 같다. 그러므로 명령어 인출과 해독의 대역폭이 획기적으로 줄어든다.
벡터 명령어를 사용하면 벡터를 구성하는 각 원소들의 계산이 벡터 내의 다른 원소들의 계산과 독립적이 되므로, 한 벡터 명령어 내에서 데이터 해저드를 점검하는 하드웨어가 필요 없다.
데이터 수준 병렬성을 가지고 있는 응용에 대해서는 벡터 구조와 벡터 컴파일러를 이용하는 것이 MIMD 멀티프로세서에서 프로그래밍 하는 것보다 훨씬 쉽다고 알려져 있다.
모든 벡터 원소 하나하나에 대해 데이터 해저드를 점검하는 대신, 벡터 피연산자 하나당 한 번씩만 점검하면 된다. 전력 소모도 그만큼 줄어든다.
메모리에 접근하는 벡터 명령어의 경우 메모리 접근 패턴을 미리 알 수 있다. 벡터의 원소들이 모두 인접해 있다면 인터리빙의 정도가 큰 인터리브 메모리에서 벡터를 인출하는 것이 매우 효율적이다. 메인 메모리에서 매 워드를 인출할 떄마다 메모리 지연을 겪는 것이 아니라 전체 벡터에 대하여 단 한 번만 지연을 겪기 때문이다.
순환문 전체가 행동이 이미 정해진 벡터 명령어 하나로 치환되기 때문에 순환문의 분기로 인한 제어 해저드가 발생하지 않는다.
이와 같이 명령어 대역폭과 해저드 점검 빈도수가 줄고 메모리 대역폭이 효율적으로 사용되기 때문에, 벡터 구조가 스칼라 구조보다 전력과 에너지 면에서 유리하다.
동일한 개수의 데이터에 대해 스칼라 연산을 수행하는 것보다 벡터 연산을 이용하면 더 빠르게 수행할 수 있다. 따라서 컴퓨터 설계자들은 벡터 연산을 자주 사용할 수 있는 응용이 있다면 벡터 유닛을 포함시키는 방향으로 가게 된다.

벡터와 멀티미디어 확장의 비교

x86 AVX 명령어와 같이 벡터 명령어도 여러 연산을 지정한다. 그러나 멀티미디어 확장이 서너 개의 연산을 지정한다면 벡터는 수십 개의 연산을 지정하는 것이 다르다.
또 멀티미디어 확장과 달리 벡터 연산에 사용되는 원소의 수는 opcode에 표시하지 않고 별도의 레지스터로 지정한다. 따라서 벡터 구조의 버전이 바뀌면서 원소의 수를 바꾸어야 할 경우 벡터 구조에서는 해당 레지스터의 값만 바꾸면 되므로 기계어 수준의 호환성을 유지할 수 있다.
하지만 MMX, SSE, SSE2, AVX, AVX2 등과 같은 x86 멀티미디어 확장에서는 '벡터'의 크기가 바뀔 때마다 많은 opcode를 새로 추가해야 한다.
또한 멀티미디어 확장과 달리 꼭 인접한 데이터를 사용해야 한다는 제약이 없다.
벡터는 하드웨어적으로 매 n번째의 데이터를 적재하도록 하는 일정한 간격의 접근(strided access)과 아울러 벡터 레지스터에 적재할 원소의 주소를 하드웨적으로 계산하는 인덱스 접근(indexed access)를 지원한다.
인덱스를 이용하여 메모리에 산재된 데이터를 인접한 벡터 원소에 적재하고 인덱스를 이용하여 벡터 원소를 메모리에 분산시키기 때문에 인덱스 접근을 '수집-분산(gather-scatter)'라고 부르기도 한다.
멀티미디어 확장처럼 벡터도 데이터의 폭을 쉽게 바꿀 수 있다. 즉 64비트 데이터 32개, 32비트 데이터 64개, 16비트 데이터 128개, 8비트 데이터 256개를 동일한 벡터 연산으로 처리할 수 있다.
벡터 명령어의 병렬성은 깊이가 깊은 파이프라인 기능 유닛을 사용하든지, 어레이 구조의 병렬 기능 유닛을 사용하든지, 혹은 파이프라인 기능 유닛과 병렬 기능 유닛을 조합해서 사용함으로써 구현할 수 있다.
그림 6.3은 벡터 덧셈 명령어를 수행함에 있어 병렬 파이프라인을 이용하면 어떻게 벡터 성능을 높일 수 있는지를 보여준다.
벡터 연산 명령어는 일반적으로 벡터 레지스터의 N번째 원소와 다른 벡터 레지스터의 N번째 원소끼리 연산을 수행하도록 지정한다. 이 때문에 병렬로 수행되는 벡터 유닛의 수를 늘리는 것이 간단해지는데, 이런 구조를 다중 병렬 벡터 레인(vector lane)이라고 명명한다.
고속도로에서와 같이 레인을 하나 더 추가하면 벡터 유닛의 최대 처리량을 증가시킬 수 있다. 그림 6.4는 4개의 레인을 갖는 벡터 유닛의 구조를 보여준다. 한 개의 레인에서 4개의 레인으로 바꾸면 벡터 명령어 하나당 클럭 사이클 수가 대략 4배로 줄어든다.
레인을 여러 개 갖는 것이 유리하기 위해서는 응용과 컴퓨터 구조가 모두 긴 벡터를 지원해야 한다. 그렇지 않다면 명령어가 너무 빨리 끝나서 더는 수행할 명령어가 없게 될 것이므로 4장의 명령어 수준 병렬 기술을 사용해서 충분한 수의 벡터 명령어를 공급해야 할 것이다.
일반적으로 벡터 구조는 데이터 병렬처리 프로그램의 수행에 매우 효율적이며 멀티미디어 확장보다 컴파일러 기술과도 더 잘 맞는다. 또한 x86 구조에 멀티미디어 확장을 하는 것과 비교할 때 시간에 따라 점진적으로 발전시키기가 더 쉽다.

하드웨어 멀티스레딩

프로그래머의 관점에서 특별히 MIMD와 관련이 깊은 개념으로 하드웨어 멀티스레딩(hardware multithreading)이 있다.
MIMD가 여러 프로세서들을 바쁘게 만들기 위해서 여러 개의 프로세스(process) 또는 스레드(thread)를 사용하는 반면, 하드웨어 멀티스레딩은 여러 스레드가 단일 프로세서의 기능 유닛들을 겹쳐서 사용함으로써 사드웨어 자원의 효율성을 높인다.
이와 같은 방식의 공유를 위해서 프로세서는 각 스레드의 독립적인 상태를 복제해야 한다. 예컨대 모든 스레드는 레지스터 파일과 프로그램 카운터의 복사본을 따로 갖고 있다. 메모리는 가상 메모리 메커니즘을 통하여 공유되는데 이것은 이미 멀티프로그래밍 지원을 위해 쓰이고 있는 기능이다.
이 외에도 하드웨어는 스레드를 빨리 전환하는 기능을 지원해야 한다. 특시 스레드 전환은 수백, 수천 사이클이 소모되는 프로세스 전환보다 훨씬 효율적으로 수행되어 순식간에 이루어져야 한다.
하드웨어 멀티스레딩에는 2가지 주요한 방식이 있다. 작은 단위 멀티스레딩(fine-grained multithreading)은 매 명령어마다 스레드를 전환하여 여러 개의 스레드를 인터리빙 하는 방식이다.
해당 사이클에 지연 상태에 있는 스레드는 건너뛰면서 라운드 로빈(round-robin) 방식으로 인터리빙 하는 것이 일반적이다.
작은 단위 멀티스레딩이 실용성을 가지려면 매 클럭 사이클만다 스레드를 전환할 수 있어야 한다. 작은 단위 멀티스레딩의 핵심적인 장점은 한 스레드가 지연되고 있는 동안 다른 스레드의 명령어를 수행함으로써 짧은 지연 혹은 긴 지연으로 말미암는 처리량의 손실을 감출 수 있다는 점이다.
반면에 가장 큰 단점은 개별 스레드의 수행이 느려진다는 점이다. 지연 없이 바로 수행될 수 있는 스레드라 할지라도 다른 스레드의 명령어 수행 때문에 지체되기 때문이다.
큰 단위 멀티스레딩(coarse-grained multithreading)은 작은 단위 멀티스레딩의 대안으로 고안되었다. 큰 단위 멀티스레딩에서는 마지막 단계 캐시의 실패와 같이 긴 지연이 생길 때만 스레드 전환을 수행한다.
이렇게 하면 스레드 전환 비용이 매우 작아야 할 필요가 없으며, 긴 지연이 발생하는 경우에만 다른 스레드의 명령어를 실행하기 때문에 개별 스레드의 수행이 느려지는 문제도 크게 완화된다.
그러나 큰 단위 멀티스레딩은 작은 지연들로 인한 처리량의 손실을 극복하는데 한계가 있다는 단점이 있다. 이 문제는 큰 단위 멀티스레딩의 파이프라인 초기화 비용 때문에 생긴다.
큰 단위 멀티스레딩은 단일 스레드에서 명령어를 가져오기 때문에 지연이 발생하면 파이프라인을 비우거나 멈추어야 한다. 지연 이후에 수행되는 새로운 스레드는 파이프라인을 새로 채워야 한다.
이와 같은 초기화 비용 때문에 파이프라인을 채우는 시간을 무시할 수 있을만큼 지연시간이 긴 경우에 더 유용하다.
동시 멀티스레딩(SMT: simultaneous multithreading)은 하드웨어 멀티스레딩의 변형으로 다중 내보내기(multile-issue)와 동적 스케쥴링을 지원하는 파이프라인 프로세서의 자원을 이용하여 명령어 수준의 병렬설 뿐만 아니라 스레드 수준의 병렬성도 함께 이용한다.
다중 내보내기 프로세서들은 일반적으로 단일 스레드가 효과적으로 사용할 수 있는 것보다 더 많은 기능 유닛 병렬성을 가지고 있다는 점에 착안하여 SMT가 등장하게 되었다.
더욱이 레지스터 재명명과 동적 스케쥴링(4장 참조)을 사용하면서 서로 다른 스레드에서 가져온 명령어 여러 개를 서로의 의존 관계에 신경 쓰지 않고 내보낼 수 있다. 의존 관계는 동적 스케쥴링 기능이 해결할 수 있기 때문이다.
SMT는 기존의 동적 메커니즘을 사용하기 때문에 매 사이클마다 자원을 바꾸지 않는다. 대신 늘 여러 스레드의 명령어들을 함께 실행하고 명령어 슬롯이나 재명명된 레지스터를 적절한 스레드에 할당하는 일은 하드웨어에 맡긴다.
그림 6.5는 프로세서 구성에 따라 수퍼스칼라 자원을 활용하는 능력이 어떻게 다른지를 개념적으로 보여준다.
그림의 상단부는 멀티스레딩을 지원하지 않는 수퍼스칼라 프로세서에서 4개의 스레드가 따로따로 수행되는 것을 보여준다.
하단부는 다음 세 가지 멀티스레딩 옵션에 따라 4개의 스레드가 어떻게 더 효율적으로 실행될 수 있는지를 보여준다.
큰 단위 멀티스레딩을 지원하는 수퍼스칼라
작은 단위 멀티스레딩을 지원하는 수퍼스칼라
동시 멀티스레딩을 지원하는 수퍼스칼라
하드웨어 멀티스레딩을 지원하지 않는 수퍼스칼라에서는 명령어 수준 병렬서잉 부족하기 때문에 내보내기 슬롯의 사용이 제한을 받는다. 게다가 명령어 캐시 실패같이 큰 지연이 발생하면 전체 프로세서가 아무 일도 하지 않고 놀게 된다.
큰 단위 멀티스레딩 수퍼스칼라에서 긴 지연이 발생하면 다른 스레드로 전환하여 이를 일부 감출 수 있다. 이렇게 하면 아무 일도 하지 않는 클럭 사이클 수를 줄일 수는 있지만, 파이프라인의 초기화 오버헤드 때문에 여전히 아무 일도 하지 않는 사이클이 생기며, 명령어 수준 병렬성이 부족하므로 모든 내보내기 슬롯을 사용하는 것이 불가능하게 된다.
작은 단위 멀티스레딩의 경우는 스레드의 인터리빙을 통하여 아무 일도 하지 않는 사이클을 대부분 없앨 수 있다. 그러나 한 클럭 사이클에서는 하나의 스레드만 명령어를 내보내기 때문에 명령어 수준의 병렬성이 부족하여 여전히 일부 클럭 사이클에서는 노는 슬롯이 생기게 된다.
SMT의 경우에는 스레드 수준 병렬성과 명령어 수준 병렬성이 모두 활용되어 한 클럭 사이클에서 여러 스레드가 내보내기 슬롯을 같이 사용한다.
이상적인 경우라면 여러 스레드가 필요로 하는 자원과 실제 가용 자원 간의 불균형이 내보내기 슬롯의 활용도를 제한하게 된다. 그러나 실제로는 다른 요인들이 슬롯 사용을 제약할 수 있다.
그림 6.5가 프로세서들의 실제 동작을 단순화하기는 했지만 멀티스레딩이 갖는 잠재적 성능상의 이점, 특히 SMT의 장점을 잘 보여주고 있다.
그림 6.6은 두 개의 스레드를 하드웨어적으로 지원하는 Intel Core i7 960의 한 프로세서에서 멀티스레딩을 이용하는 경우의 성능과 에너지 측면에서의 이득을 도시하고 있다.
평균 1.31배의 속도 개선을 얻었는데 하드웨어 멀티스레딩을 위한 추가 자원의 규모가 크지 않음을 고려하면 나쁘지 않은 결과이다. 평균 에너지 효율은 1.07이며 이는 아주 뛰어난 결과이다. 일반적으로 에너지 소모를 늘리지 않으면서 성능을 높일 수 있으면 매우 만족스럽다.

멀티코어와 기타 공유 메모리 멀티프로세서

하드웨어 멀티스레딩이 추가 비용을 크게 들이지 않고 프로세서의 효율을 높이기는 했지만, 칩에 집적되는 프로세서의 수가 많아지면서 어떻게 효율적으로 프로그래밍을 해서 Moore의 법칙대로 성능 개선을 이룰 수 있을까 하는 문제는 지난 10년간의 큰 과제였다.
기존 프로그램이 병렬 하드웨어에서 잘 수행되게 고치는 것은 어렵기 때문에, 자연스럽게 '컴퓨터 설계자들이 이 작업을 쉽게 만들기 위해 할 수 있는 일이 없을까?' 라는 생각을 하게 되었다.
한 가지 답은 모든 프로세서들이 공유하는 단일 실제 주소 공간을 제공하는 것이었다. 이렇게 되면 프로그램들은 자신들이 사용할 데이터가 어디에 있는지 신경 쓸 필요가 없이 병렬적으로 수행될 수 있다. 이 방식에서는 언제나 또 어느 프로세서나 프로그램의 모든 변수에 접근할 수 있다.
다른 방식은 프로세서마다 별도의 주소공간을 갖고 공유할 것이 있으면 명시적으로 표시하는 방식인데, 이에 대해서는 6.7절에서 살펴볼 것이다. 실제 주소공간이 공유될 때는 하드웨어가 캐시 일관성을 보장하여 모든 프로세서가 공유 메모리에 대해 일치된 관점(consistnet view)을 가지도록 하는 것이 일반적이다. (5.8절 참조)
앞서 말한 바와 같이 공유 메모리 멀티프로세서(SMP; shared memory multi-processor)에서는 모든 프로세서가 단일 실제 주소공간을 가지며 멀티코어 칩은 거의 다 이런 방식으로 구현된다. 어쩌면 공유 주소 멀티프로세서라고 부르는 것이 더 정확한 표현일지도 모르겠다.
프로세서들은 어느 메모리 주소든지 적재와 저장 명령어를 사용하여 접근할 수 있으며 프로세서 간의 통신은 메모리에 있는 공유변수를 통하여 이루어진다. 그림 6.7은 SMP의 전형적인 구조를 보여준다.
프로세서들이 실제 주소공간을 공유하더라도 작업들은 각각 별개의 가상 주소공간에서 독립적으로 수행될 수 있음에 유의하자.
단일 주소공간 멀티프로세서에는 두 가지 스타일이 있다. 한 가지는 어느 프로세서가 어느 워드에 접근하든지 간에 동일한 시간이 걸리는 스타일인데 이와 같은 것을 균일 메모리 접근(UMA; unifor memory access) 멀티프로세서라고 한다.
두 번째 스타일에서는 어떤 프로세서가 어떤 워드에 접근하는지에 따라 메모리 접근 시간이 달라진다. 이것은 메인 메모리가 여러 조각으로 분할되어서 각각이 다른 마이크로프로세서 또는 동일 칩 내의 다른 메모리 제어기에 붙어 있기 때문이다. 이러한 기계를 비균일 메모리 접근(NUMA; nonuniform memory access) 멀티프로세서라고 한다.
NUMA 멀티프로세서가 UMA 멀티프로세서보다 프로그래밍하기 어렵다는 것은 예상할 수 있을 것이다. 그러나 NUMA 멀티프로세서는 가까운 메모리에 빨리 접근할 수 있고 규모를 크게 만들 수 있다는 장점이 있다.
동시에 동작하는 프로세서들끼리는 데이터를 공유하는 것이 일반적이므로 공유 데이터에 대한 작업을 할 때는 조정이 필요하다.
조정이 이루어지지 않으면 어느 프로세서가 공유 데이터에 대한 작업을 끝내기 전에 다른 프로세서가 같은 데이터에 대한 작업을 시작할 수 있을 것이다. 이 조정 작업을 동기화(synchronization)라 하며 단일 주소공간에서 공유가 허용되는 경우에는 동기화를 위한 별도의 메커니즘이 필요하다.
한 가지 방법은 공유변수에 대한 잠금변수(lock)를 이용하는 것이다. 한 번에 한 프로세서만 잠금변수를 얻을 수 있고, 공유 변수를 사용하고자 하는 다른 프로세서들은 그 프로세서가 잠금을 풀 때까지 기다려야만 한다. 2.10절에서 잠금에 사용할 수 있는 MIPS 명령어를 소개한 바 있다.

예제) 공유 주소공간에서의 단순한 병렬처리 프로그램

UMA 구조의 공유 메모리 멀티프로세서 컴퓨터에서 64,000개의 숫자를 더하는 프로그램을 작성하라. 프로세서의 수는 64개라고 가정한다.
첫 번째 단계는 숫자들을 동일한 크기의 묶음으로 나누어서 프로세서 간의 부하를 균등하게 만드는 것이다. 이 컴퓨터는 단일 주소공간을 갖고 있으므로 나눈 묶음들을 다른 메모리 공간에 할당할 필요는 없다.
각 프로세서마다 서로 다른 시작 주소를 지정해 주기만 하면 된다. Pn이 0부터 63까지의 프로세서 번호라고 하자. 모든 프로세서는 자기에게 할당된 숫자들을 더하는 순환문으로 프로그램 수행을 시작한다.
sum[Pn] = 0; for (i = 1000*pn; i < 1000*(Pn+1); i +=1) { sum[Pn] += A[i]; /* sum the assigned areas */ }
C++
그 다음 단계는 64개의 부분합들을 더하는 것이다. 이 단계를 리덕션(reduction)이라고 한다. 여기서는 분할정복(divide-and-conquer) 방법을 사용한다. 프로세서들의 절반이 부분합 두 개씩을 더하고, 다시 프로세서의 1/4이 새롭게 얻은 부분합 두 개씩을 더한다. 합이 하나가 될 때까지 이 과정을 반복한다. 그림 6.8이 이러한 리덕션 과정을 보여준다.
이 예제에서 '생산자' 프로세서가 메모리 위치에 값을 쓴 이후에 '소비자' 프로세서가 그 값을 읽어 가도록 두 프로세서 간에 동기화가 이루어져야 한다. 그렇지 않다면 소비자가 데이터의 이전 값을 읽을 수도 있기 때문이다.
그리고 각 프로세서가 반복제어 변수 i를 자신만 사용하도록 하기 위해 지역변수로 지정하여야 한다. 프로그램은 다음과 같다. (half도 지역변수이다.)
half = 64; /* 64 processor in multiprocessor */ do { synch(); /* wait for partial sum completion */ if (half % 2 != 0 && Pn == 0) { sum[0] += sum[half-1]; /* Conditional sum needed when half is odd; Processor0 gets missing element */ half = half / 2; if (Pn < half) { sum[Pn] += sum[Pn+half]; } } } while (half > 1); /* exit with final sum in Sum[0] */
C++
병렬 프로그래밍에 대한 관심이 오래된 만큼, 병렬 프로그래밍 시스템을 만들기 위한 시도가 수백 번이나 있었다.
용도가 제한적이기는 하지만 널리 알려진 예로 OpenMP가 있다. 이것은 컴파일러 지시어, 환경 변수, 그리고 표준 프로그래밍 언어를 확장할 수 있는 런타임 라이브러리 루틴이 붙은 API(Application Programmer Interface)에 불과하다.
OpenMP는 공유 메모리 멀티프로세서를 위한 이식성 있고 확장성 있는 단순한 프로그래밍 모델을 제공한다. 주된 목적은 순환문을 병렬화하고 리덕션을 수행하는 것이다.
대부분의 C 컴파일러는 이미 OpenMP를 지원하고 있다. UNIX C에서 OpenMP API를 사용하기 위한 명령어는 다음과 같다.
cc -fopenmp foo.c
C++
OpenMP는 pragma를 이용하여 C 언어를 확장하는데 이것은 #define이나 #include처럼 C 매크로 전처리기를 위한 명령이다. 이 예제와 같이 프로세서 개수를 64개로 정해 주려면 다음 명령을 사용하면 된다.
#define P 64 /* define a constant that we'll use a few times */ #pragram omp parallel num_threads(P)
C++
이 말은 런타임 라이브러리가 64개의 병렬 스레드를 사용할 것이라는 뜻이다.
순차적인 for 순환문을 병렬 for 순환문으로 바꾸기 위해서 우리가 지정한 개수의 스레드 수만큼 작업을 나누어야 하며, 이는 다음과 같이 작성하면 된다. (sum은 0으로 초기화되어 있다고 가정한다)
#pragma omp parallel for for (Pn = 0; Pn < P; Pn += 1) { for (i = 0; i < 1000 * Pn; i < 1000 * (Pn+1); i +=1) { sum[Pn] += A[i]; /* sum the assigned areas */ } }
C++
리덕션을 수행하기 위해서 리덕션 연산의 종류와 리덕션 결과를 넣을 변수를 OpenMP에게 알려 주는 명령을 사용할 수 있다.
#pragma omp parallel for reduction(+ : FinalSum) for (i = 0; i < P; i += 1) { FinalSum += sum[i]; /* Reduce to a single number */ }
C++
64개의 프로세서를 이용하여 64개의 숫자를 더하는 효율적인 코드를 찾는 것은 OpenMP 라이브러리에게 달린 문제임에 유의하라
OpenMP가 간단한 병렬 코드 작성을 쉽게 해 주기는 하지만, 디버깅에는 큰 도움이 되지 않는다. 그래서 오늘날 많은 프로그래머들이 C보다 생산성이 더 좋은 다른 언어를 사용하는 것과 마찬가지로 OpenMP보다 더 정교한 병렬 프로그래밍 시스템을 사용하는 병렬 프로그래머들이 많아지고 있다.

그래픽 처리 유닛의 기초

기본 구조에 SIMD 명령어를 추가하는 것이 타당하다고 생각하게 된 초창기의 이유는 그래픽을 처리하는 시간의 비중이 점점 늘어난다는 것이었다.
그래픽 처리 성능을 끌어올리는 주 동력은 게임 산업이었다. 여기에는 PC와 PlayStation과 같은 전용 게임이가 모두 포함된다. 빠르게 성장하는 게임 시장은 많은 회사들로 하여금 더 빠른 그래픽 하드웨어 개발에 더 많은 투자를 하도록 하였다.
이런 선순환 구조는 주류 마이크로프로세서의 범용 처리 성능보다 더 빠른 속도로 그래픽 처리 성능이 개선되게 하였다.
그래픽과 게임 커뮤니티는 마이크로프로세서를 개발하는 커뮤니티와 다른 목표를 가지고 있기 때문에 독특한 스타일의 처리 기술과 용어들이 발전하였다. 그래픽 프로세서가 점점 더 큰 세력을 얻게 됨에 따라 CPU와 구별하기 위해 그래픽 처리 유닛(GPU)라는 이름을 사용하게 되었다.
오늘날 200-300 달러만 있으면 수백 개의 병렬 부동소수점 유닛을 장착한 GPU를 구매할 수 있는데, 그 덕분에 고성능 컴퓨팅을 쉽게 접할 수 있게 되었다. 이런 잠재적인 성능이 GPU를 쉽게 프로그래밍하도록 하는 프로그래밍 언어와 접목되면서 GPU에 대한 관심이 커지게 되었다.
그래서 오늘날 과학 계산이나 멀티미디어 응용 프로그램을 작성하는 사람 중에는 GPU를 사용할지 CPU를 사용할지 고민하는 사람이 많다.
GPU와 CPU의 주요 차이점은 다음과 같다.
GPU는 CPU를 돕는 가속기다. 따라서 CPU가 수행하는 모든 작업을 다 수행할 필요가 없다. 그렇기 때문에 모든 자원을 그래픽만을 위해 사용할 수 있다. GPU가 어떤 작업을 전혀 수행할 수 없거나 느리게 수행한다고 해도 그 시스템이 CPU와 GPU를 모두 가지고 있다면 문제가 되지 않는다. CPU가 그 일을 담당하면 되기 때문이다.
GPU 문제의 크기는 보통 수백 MB에서 GB 수준에 이른다. 수백 GB가 TB까지 가지는 않는다.
이런 차이점 때문에 다른 스타일 구조를 갖게 되었다.
아마 가장 큰 차이점은 GPU의 경우 메모리의 큰 지연시간을 극복하기 위해서 CPU처럼 다단계 캐시를 사용하지 않는다는 점일 것이다. 그 대신 GPU는 메모리 지연을 감추기 위해서 하드웨어 멀티스레딩을 사용한다. 즉 메모리 접근을 요청한 후 그 데이터가 도착할 때까지 GPU는 그 요청과 무관한 스레드를 수백, 수천 개 수행한다.
GPU의 메인 메모리는 지연시간보다는 처리율을 높이는데 초점을 맞춘다. CPU를 위한 DRAM 칩보다 폭이 넓고 처리량이 큰 DRAM 칩을 GPU 전용으로 두기도 한다.
GPU 메모리는 통산 마이크로프로세서의 메인 메모리보다 더 작다. 2013년 기준으로 PCU가 32 GiB에서 256 GiB까지의 메모리를 사용하는 반면, GPU는 통상 4 GiB에서 6 GiB나 그 이하의 메모리를 갖는다.
끝으로 GPU는 코프로세서이므로 범용 연산을 할 때는 CPU 메모리와 GPU 메모리 간의 전송 시간이 추가된다.
큰 메모리 대역폭을 얻기 위해서 많은 수의 스레드를 활용하기 때문에 GPU는 많은 스레드와 많은 병렬 프로세서(MIMD)를 수용할 수 있다. 따라서 전형적인 CPU와 비교할 때 GPU는 훨씬 고도로 멀티스레딩되어 있고 프로세서도 더 많다.
GPU는 좁은 응용 범위를 대상으로 설계되었지만, 일부 프로그래머들은 GPU의 높은 잠재 성능을 이용할 수 있도록 응용 프로그램을 작성할 수 있는지가 궁금하였다.
처음에는 그래픽 API와 언어를 사용하여 문제를 서술하려고 해 보다가 여기에 지친 사람들이 직접 GPU 프로그램을 작성할 수 있도록 C 언어에 근간을 둔 프로그램 언어를 개발하였다. NVIDIA 사의 CUDA(Computer Unified Device Architecture)가 한 예인데, 일부 제약이 있기는 하지만 프로그래머가 GPU에서 수행될 C 프로그램을 직접 작성할 수 있게 해준다.
OpenCL은 CUDA가 갖는 많은 장점을 제공하면서도 이식성 있는 프로그래밍 언어를 개발하기 위해 여러 기업이 함께 추진하고 있는 사업이다.
NVIDIA는 여러 형태의 병렬성을 통합하는 주제를 CUDA 스레드로 정하였다. 가장 낮은 수준의 이 병렬성을 프로그래밍 프리미티브로 사용하여 컴파일러와 하드웨어가 수천 개의 CUDA 스레드를 함께 묶어서 멀티스레딩, MIMD, SIMD 명령어 수준의 병렬성 등 GPU 내의 다양한 병렬성을 활용할 수 있다. 이 스레드들은 32개씩 묶여져서 블록을 이루고 한꺼번에 수행된다.
GPU 안에 있는 멀티스레드 프로세서는 이 스레드 블록들을 실행하는데, GPU는 8-32개의 멀티스레드 프로세서로 구성된다.

NVIDIA GPU 구조의 기초

GPU 구조를 대표하는 예로 NVIDIA 시스템을 사용하려고 한다. 특히 CUDA 병렬 프로그래밍 언어의 용어를 그대로 사용하고 Fermi 구조를 예로 들어 살펴본다.
벡터 구조처럼 GPU는 데이터 수준 병렬 문제를 잘 처리한다. 두 방식 모두 수집-분산 방식의 데이터 이동을 지원하는데 GPU 프로세서가 벡터 프로세서보다 더 많은 레지스터를 갖고 있다. 대부분의 벡터 프로세서와 달리 GPU는 한 멀티스레드 SIMD 프로세서 내의 하드웨어 멀티스레딩을 사용하여 메모리 지연을 감춘다.
멀티스레딩 SIMD 프로세서는 벡터 프로세서와 비슷하다. 그러나 깊게 파이프라이닝 되어 있는 기능 유닛을 몇 개 가지고 있는 벡터 프로세서와 달리 GPU는 많은 수의 병렬 기능 유닛을 갖고 있다.
앞서 언급한 바와 같이 GPU는 멀티스레드 SIMD 프로세서들을 가지고 있다. 즉 GPU는 멀티스레드 SIMD들로 구성된 MIMD이다.
NVIDIA는 Fermi 구조의 GPU를 가격대별로 각각 7, 11, 14, 15개의 멀티스레드 SIMD를 갖도록 구현하고 있다. 멀티스레드 SIMD 프로세서의 수가 다른 여러 모델의 GPU에 투명한 확장성을 제공하기 위해서 스레드 블록 스케쥴러 하드웨어가 스레드 블록을 멀티스레드 SIMD 프로세서에 할당한다.
그림 6.9는 멀티스레드 SIMD 프로세서의 간략화된 블록 다이어그램을 보여준다.
한 단계 더 상세히 설명하자면, 하드웨어가 생성하고 관리하며 스케쥴링하고 실행하는 객체는 SIMD 명령어들의 스레드이다. 이를 SIMD 스레드라고 부른다. 이 SIMD 스레드들은 독자적인 프로그램 카운터를 가지고 있으며 멀티스레드 SIMD 프로세서에서 수행된다.
SIMD 스레드 스케쥴러는 어떤 SIMD 스레드가 수행할 준비가 되어 있는지를 알려주는 제어기를 포함하고 있으며 수행할 SIMD 스레드를 디스패치 유닛에 전달하여 멀티스레드 SIMD 프로세서에서 수행되도록 한다.
이것은 SIMD 명령어로 구성된 스레드를 스케쥴링 한다는 점을 제외하고는 전통적인 멀티스레드 프로세서에서의 하드웨어 스레드 스케쥴러와 동일하다. (6.4절 참조)
그러므로 GPU 하드웨어는 두 단계의 하드웨어 스케쥴러를 갖는다.
1.
스레드 블록을 멀티스레드 SIMD 프로세서에 할당하는 스레드 블록 스케쥴러
2.
SIMD 내에서 SIMD 스레드를 스케쥴링하는 SIMD 스레드 스케쥴러
이 스레드의 SIMD 명령어는 폭이 32이어서 SIMD 명령어의 각 스레드는 32개의 원소에 대하여 계산을 수행한다. 스레드가 SIMD 명령어로 구성되어 있으므로 SIMD 프로세서는 연산을 수행하기 위해서 병렬 기능 유닛을 가져야 한다. 우리는 이것을 SIMD 레인이라고 하며 6.3절에서 살펴본 벡터 레인과 매우 유사하다.

NVIDIA GPU 메모리 구조

그림 6.10은 NVIDIA GPU의 메모리 구조를 보여준다. 멀티스레드 SIMD 프로세서 내에 존재하는 온칩 메모리를 지역 메모리(local memory)라고 부른다.
멀티스레드 SIMD 프로세서 내에 있는 SIMD 레인은 지역 메모리로 서로 공유하지만 다른 멀티스레드 SIMD 프로세서와는 공유하지 않는다. 전체 GPU와 모든 스레드 블록이 공유하는 DRAM은 칩 외부에 존재하며 GPU 메모리(GPU Memory)라고 부른다.
응용 프로그램의 모든 워킹셋(working set)을 담을 수 있는 큰 캐시 대신에 GPU는 전통적으로 작은 크기의 스트리밍 캐시를 사용하고 DRAM의 긴 지연시간을 감추기 위해서 SIMD 명령어 스레드들의 멀티스레딩을 대대적으로 사용한다.
그 이유는 워킹셋의 크기가 수백 MB나 되어서 멀티코어 마이크로프로세서의 마지막 단계 캐시에 들어가지 않기 때문이다.
DRAM 메모리 지연시간을 감추기 위해서 하드웨어 멀티스레딩을 사용하므로, 시스템 프로세서에서 캐시에 사용되는 칩 영역을 컴퓨팅 자원과 많은 레지스터 구현에 사용하였다. SIMD 명령어의 많은 스레드의 상태를 저장하기 위해서 레지스터가 많이 필요하기 때문이다.

한 발짝 떨어져서 본 GPU

상위 수준에서 GPU는 SIMD 명령어를 확장한 멀티코어 컴퓨터와 유사한 면이 많다. 그림 6.11에 유사점과 차이점이 정리되어 있다.
GPU가 더 많은 프로세서와 훨씬 더 많은 수의 SIMD 레인을 가지고 있지만, 두 개 모두 여러 개의 SIMD 레인을 갖는 프로세서로 구성된 MIMD이다.
GPU가 훨씬 더 많은 스레드를 지원하지만 두 개 모두 캐시를 사용하는데, GPU는 작은 크기의 스트리밍 캐시를 사용하는 반면, 멀티코어 컴퓨터는 되도록 워킹셋 전체를 담을 수 있을만큼 큰 다단계 캐시를 사용한다. GPU의 실제 메인 메모리의 크기가 훨씬 작지만, 두 개 모두 64비트 주소공간을 사용한다.
GPU는 페이지 단위에서 메모리를 보호하는 기능을 지원하지만 아직 요구 페이징(demand paging)은 지원하지 않는다.
SIMD 프로세서는 벡터 프로세서와도 비슷하다. GPU의 SIMD 프로세서들은 독립적인 MIMD 코어처럼 동작하는데, 이것은 벡터 컴퓨터들이 여러 벡터 프로세서를 가지고 있는 것과 같다.
이 관점에서 보면 Fermi GTX 580은 코어당 16개의 레인이 있고 하드웨어 멀티스레딩을 지원하는 16-코어 기계로 볼 수 있다.
가장 큰 차이점은 멀티스레딩으로 GPU에서는 기본적인 기능이지만 대부분의 벡터 프로세서에는 없다.
컴퓨터 구조의 계보를 따라 올라가도 CPU와 GPU의 공통 조상은 찾을 수 없다. 다시 말하면 이 둘 간의 잃어버린 고리(Missing Link)가 존재하지 않는 것이다.
이렇게 혈통이 다르기 때문에 GPU는 컴퓨터 구조 커뮤니티에서 사용하는 용어를 사용하지 않는데, 이로 인해 GPU가 어떤 것이고 어떻게 동작하는지를 이해하는데 혼란이 생겼다.
이런 혼란을 줄이기 위해 이 절에서 사용한 서술적 용어, 주류 컴퓨팅 용어에 가장 근접한 용어 NVIDIA GPU에서 공식적으로 사용하는 용어를 그림 6.12에 왼쪽에서 오른쪽 순서로 보였다.
GPU가 주류 컴퓨팅으로 이동하고 있지만 그래픽을 처리하는데 뛰어난 성능을 보여야 한다는 책임을 포기할 수는 없다. 그러므로 "그래픽을 잘 처리하기 위해서 하드웨어를 투자했다. 그럼 어떻게 보완을 하면 더 넓은 분야의 응용에서도 성능이 개선될 수 있을까?"라는 관점에서 GPU를 설계해야 사리에 맞을 것이다.

클러스터, 창고 규모의 컴퓨터와 기타 메시지 전달 멀티프로세서

주소공간을 공유하는 대신 프로세서들이 자신만의 실제 주소공간을 갖도록 하는 방법이 있다. 그림 6.13은 다수의 전용 주소공간(private address space)를 사용하는 멀티프로세서의 전형적인 구성을 보여준다.
이 방식의 멀티프로세서는 메시지 전달 컴퓨터라고 부르는데 명시적인 메시지 전달(message passin) 방식으로 프로세서 간 통신이 이루어진다.
시스템이 메시지 전송 루틴(send message routine)과 메시지 수신 루틴(receive message routine)을 가지고 있는 경우, 송신 프로세서는 메시지 전송 시점을 알고 수신 프로세서는 메시지 도착 시점을 알기 떄문에 프로세서 간의 조정이 메시지 전달 자체로 이루어진다.
송신 측이 메시지 도착 여부를 확인할 필요가 있을 때는 수신 프로세서가 송신 프로세서에게 확인 메시지를 보낼 수 있다.
고성능의 메시지 전달 연결망을 기반으로 고성능 컴퓨터를 구성하려는 여러 시도가 있었다. 이들은 근거리 네트워크(LAN)을 사용하여 구성한 클러스터보다 통신 성능의 절대치 측면에서 우수하다. 실제로 오늘날의 많은 수퍼컴퓨터가 특화된 연결망을 사용한다.
그러나 문제는 이런 특화된 연결망이 이더넷과 같은 LAN보다 훨씬 비싸다는 것이다. 실제로 그렇게 많은 돈을 들여서 통신 성능을 좋게 하는 것을 정당화할 만한 응용은 고성능 컴퓨팅 분야 외에는 거의 없다.
하드웨어 설계자에게는 캐시 일관적 공유 메모리(cache-coherent shared memory) 컴퓨터보다 메시지 전달을 이용하여 통신하는 컴퓨터를 만드는 것이 훨씬 쉽다. (5.8절 참조)
또한 프로그래머 입장에서는 통신이 명시적으로 표현되기 때문에 공유 메모리 컴퓨터의 암묵적 통신과 달리 성능을 어느 정도 예측할 수 있다는 장점이 있다.
반면에 단점은 순차적인 프로그램을 메시지 전달 컴퓨터에 이식시키는 것이 힘들다는 것이다. 통신이 일어나야 할 부분을 사전에 모두 표시하지 않으면 프로그램이 동작을 하지 않을 것이기 때문이다. 캐시 일관적 공유 메모리에서는 하드웨어가 통신이 필요한 데이터를 파악하기 때문에 이식이 쉽다.
암묵적 통신이 장점도 있고 단점도 있기 때문에 고성능을 얻기 위한 가장 빠른 길이 무엇인지에 대해서는 이견이 많다. 하지만 시장에서는 그러한 혼란이 없다. 멀티코어 마이크로프로세서는 공유 메모리를 사용하는 반면, 클러스터들의 노드들은 메시지 전달을 이용하여 서로 통신한다.
어떤 병행 응용은 공유 메모리 구조이든 메시지 전달 구조이든 관계없이 병렬 하드웨어에서 잘 수행된다. 특히 태스크 수준 병렬성이나 웹 검색이나 파일 서버와 같이 통신이 거의 없는 응용은 공유 주소를 사용해야만 잘 수행되는 것은 아니다.
그래서 메시지 전달 병렬 컴퓨터 중에서 클러스터(cluster)가 오늘날 가장 널리 쓰이고 있다. 클러스터의 각 노드는 독자적인 메모리와 운영체제를 가지고 있다.
반면에 마이크로프로세서 내의 코어들은 칩 내의 고성능 연결망으로 연결되어 있으며, 멀티칩 공유 메모리 시스템은 메모리 연결망으로 연결되어 있다. 메모리 연결망은 대역폭이 더 크고 지연 시간은 작기 때문에 공유 메모리 멀티프로세서의 통신 성능이 훨씬 좋다.
병렬 프로그래밍 관점에서는 사용자 메모리를 따로따로 갖고 있다는 것이 약점이기도 하지만 시스템 신용도 측면에서는 오히려 강점이 된다. (5.5절 참조)
클러스터는 근거리 네트워크로 연결된 독립적인 컴퓨터로 구성되므로 시스템 전체를 중단시키지 않고 컴퓨터 하나를 대체하는 것이 공유 메모리 멀티프로세서의 경우에 비해 훨씬 쉽다.
본래 공유 주소를 사용하는 경우 프로세서 하나를 분리하고 대체하는 것은 운영체제가 엄청난 일을 하고 서버의 물리적 설계가 기가 막히게 잘 되어 있지 않는 한 아주 어려운 일이다.
클러스터의 경우 서버가 하나 고장 나면 부드럽게 성능을 떨어뜨릴 수 있으므로 신용도가 좋아진다. 클러스터 소프트웨어는 각 컴퓨터에서 돌아가는 운영체제 위에서 수해오디기 때문에 고장난 컴퓨터를 대체하는 것이 훨씬 쉽다.
클러스터는 완전한 컴퓨터와 독립적이면서도 확장 가능한 네트워크로 구서오디기 때문에 클러스터 위에서 돌아가는 응용들을 중단시키지 않고 시스템을 확장하는 것도 쉽다.
낮은 비용, 높은 가용성, 빠르면서도 점진적으로 확장할 수 있는 특성 때문에 대규모 공유 메모리 멀티프로세서와 비교했을 때 낮은 통신 성능을 보임에도 불구하고 웹 서비스를 제공하는 사업자에게 클러스터는 매력적이다. 매일 수백만 명이 사용하는 검색 엔진들이 이 기술을 사용하고 있다.

창고 규모의 컴퓨터

인터넷 서비스를 하려면 100,000대의 서버를 설치하여 전력을 공급하고 냉각시킬 수 있는 건물의 신축이 필요하다. 이들은 단순히 큰 규모의 클러스터 분류할 수도 있겠지만, 그 구조와 동작은 훨씬 복잡하다.
건물 자체가 하나의 거대한 컴퓨터처럼 동작하며, 건물, 전력, 냉각 시설, 서버, 50,000 ~ 100,000대의 서버를 연결하는 네트워크 장비의 비용은 1억 5천만 달러를 상회한다.
우리는 이것을 새로운 종류의 컴퓨터로 간주하고 창고 규모의 컴퓨터(WSC; Warehouse Scale Computers)라고 명명한다.
WSC에서 일괄처리(batch processing)를 위해 널리 알려진 프레임워크로 MapReduce와 공개 소프트웨어인 Hadoop이 있다.
같은 이름을 가진 Lisp 함수에 착안해서 만든 Map은 프로그래머가 작성한 함수를 각각의 논리적인 입력 레코드에 적용한다. Map은 수천 개의 서버를 사용하여 키-값(key-value) 쌍 형태의 중간 결과를 생성한다.
Reduce는 이렇게 분산 수행된 태스크의 결과 값을 모아 프로그래머가 저으이한 또 다른 함수를 사용하여 통합된 결과를 산출한다.
적절한 소프트웨어 지원을 받으면 Map과 Reduce 동작은 이해하기 쉽고 사용하기 쉬운 방법을 이용하여 대규모로 병렬화하는 것이 가능하다. 초보 프로그래머라 하여도 30분 안에 MapReduce 태스크를 수천 개의 서버를 이용해서 수행할 수 있다.
한 예로 많은 문서의 모음에서 각 영어 단어가 등장하는 빈도수를 계산하는 MapReduce 프로그램을 생각해 보자. 아래의 간단한 프로그램은 어떤 한 문서에서 모든 영어 단어가 딱 한 번씩 등장한다는 것을 가정하고 작성한 프로그램의 내부 순환문이다.
map(String key, String value): // key: document anme // value: document contents for each word w in value: EmitIntermediate(w, "1"); // Produce list of all words reduce(String key, Iterator values): // key: a word // values: a list of counts int result = 0; for each v in values: result += ParseInt(v); // get integet value from key-value pair //value represents number of occurrences in all documents Emit(AsString(result));
Python
Map 함수에서 사용하는 EmitIntermediate 함수는 문서에 등장하는 각 단어와 1을 출력한다. 그리고 Reduce 함수는 ParseInt 함수를 이용하여 문서 내 각 단어의 값을 모두 더해서 모든 단어의 출현 빈도수를 구한다. MapReduce 렁나팅 환경이 map 태스크와 reduce 태스크를 WSC의 서버에 스케쥴링한다.
WSC는 1970년대의 수퍼컴퓨터를 극단적으로 크게 만들어서 전력 공급과 냉각, 모니터링, 운영 면에서 혁신을 불러 온 현대판 후손이라고 볼 수 있다. 그런 면에서 Seymour Cray는 오늘날 WSC 설계자의 대부라고 할 수 있다.
그의 극단적인 컴퓨터는 다른 어디서도 경험할 수 없는 정도의 계산을 처리할 수 있었으며, 가격이 너부 비싸서 소수의 회사만 구입할 수 있었다.
오늘날 WSC는 과학자와 공학자에게 고성능 컴퓨팅 능력을 제공하는 것 대신 세상 모든 사람에게 정보 기술을 제공하는 것을 목적으로 한다. 그러므로 Cray의 수퍼컴퓨터가 과거에 사회에 끼친 영향보다 WSC가 사회에 끼치는 영향이 더 막중함에 틀림 없다.
서버와 공통적인 목표가 몇 개 있지만, WSC는 다음 3가지 면에서 차이가 있다.
1.
대규모이면서 쉬운 병렬성
서버 설계자의 고민은 대상으로 하는 실제 응용이 병렬 하드웨어의 규모를 정당화할 정도로 충분한 병렬성을 가지고 있는지, 그리고 병렬성 활용에 충분한 성능을 갖춘 통신 하드웨어가 너무 비싼 것은 아닌지에 관한 것이다.
WSC 설계자는 그런 고민을 하지 않는다. 첫째 MapReduce와 같은 일괄 처리 응용은 웹 크롤(Web Crawl) 응용에서 수십억 개의 페이지를 탐색하는 것처럼, 대규모의 독립ㅈ거인 데이터에 대하여 독립적으로 처리할 일이 많다는 이점이 있다.
둘째로 서비스로서의 소프트웨어, 즉 Saas(Software as a Service)로 알려진 쌍방식 인터넷 서비스 응용은 수백만 명의 독립적인 사용자가 쌍방식의 인터넷 서비스를 사용하는 특징이 있다.
SaaS에서는 읽기와 쓰기가 의존적인 경우가 거의 없으므로 동기화가 거의 필요 없다. 예컨대 검색은 읽기 전용의 인덱스를 사용하며, 이메일은 서로 독립적인 정보를 읽고 쓰는 것이 보통이다.
이렇게 쉬운 병렬성의 형태를 요구 수준 병렬성(Request-Level Parallelism)이라고 칭하는데, 통신과 동기화의 필요성이 거의 없이 자연스럽게 병렬적으로 처리할 수 있는 다수의 독립적인 작업들의 행동을 의미한다.
2.
가동 비용의 중요성
전통적으로 서버 설계자는 시스템을 설계할 때 예산의 한도 내에서 최고 성능을 얻도록 하고 에너지에 대해서는 주어진 공간의 냉각 용량을 초과하지 않는 것을 보장하는 수준까지만 신경을 썼다.
서버의 구매비용에 비해서 가동 비용은 미미한 것으로 가정을 했다. 그러나 WSC는 수명이 길어서 건물과 전력 및 냉각 설비가 보통 10년 이상 감가상각된다. 그래서 10년의 기간을 따져 보면 에너지, 전력, 냉각의 가동 비용이 전체 WSC 비용의 30% 이상이 된다.
3.
확장성과 확장에 따른 기회/ 문제들
WSC 하나를 구축하기 위해서 100,000대 이상의 서버와 아울러 이를 지원하는 기반 설비를 구매해야 하므로, 대량 구매에 따른 할인을 받을 수 있다. 그러므로 WSC가 많지는 않지만, WSC 자체의 규모가 상당하므로 '규모의 경제'를 누릴 수 있다.
규모의 경제 덕택에 클라우드 컴퓨팅이 탄생하였다. WSC의 유닛당 단가가 낮기 떄문에, 클라우드 업체들이 적절한 이윤이 날 만큼 서버 임대료를 책정해도 사용자들이 직접 서버를 구매하는 것보다는 저렴하게 할 수 있다.
규모의 경제 이면에는 규모에 따라 고장 확률이 높아지는 문제가 있다. 서버 한 대의 MTTF(Mean Time To Failure)가 25년(200,000시간)이나 되지만 WSC 설계자는 매일 5대 정도의 서버가 고장나는 것을 목표로 한다. (디스크 오류율 계산 생략) 따라서 고장 감내는 서버 설계자보다 WSC 설계자에게 더 중요한 이슈이다.
WSC가 보여주는 규모의 경제는 사람들이 오랫동안 꿈꾸어 왔던 컴퓨팅이 전기, 수도와 같은 유틸리티가 되는 것을 실현하고 있다. 클라우드 컴퓨팅은 누구든지 어디서든 좋은 아이디어와 비즈니스 모델, 그리고 신용카드만 있으면 수천 개의 서버를 이용해서 전 세계로 자신들의 비전을 거의 순식간에 전달하는 것을 가능하게 한다. (이하 생략)

멀티프로세서 네트워크 위상의 기초

멀티코어 칩은 코어들을 연결하기 위해 칩 내에 네트워크가 필요하고 클러스터는 서버를 연결하기 위해 근거리 네트워크가 필요하다. 이 절에서는 여러 연결망 네트워크 위상의 장단점을 살펴본다.
네트워크 가격은 스위치 개수, 스위치 하나당 필요한 링크의 수, 링크의 폭(즉 비트 수), 실제 칩에 적용했을 때 링크의 길이 등에 의해 결정된다.
예컨대 어떤 코어나 서버들은 인접해 있는 반면 어떤 코어들은 칩의 반대편, 혹은 데이터센터의 반대편에 위치할 수도 있다.
마찬가지로 네트워크 성능도 여러 측면이 있어서 부하가 없는 네트워크에서 메시지 하나를 보내고 받는데 걸리는 시간, 주어진 시간 동안 전송할 수 있는 최대 메시지 개수로 표현되는 처리량, 네트워크 내에서 생기는 충돌에 의한 지연, 통신 패턴에 따라 달라지는 성능 등 다양한 요소를 포함한다.
또 어떤 시스템은 일부 요소가 고장 나더라도 계속 동작할 수 있어야 하기 때문에 네트워크에 고장 감내 기능이 있어야 한다. 끝으로 요즈음에는 칩에 에너지 제한이 있기 때문에 네트워크 구조의 에너지 효율이 다른 요소보다 더 중요할 수도 있다.
네트워크는 보통 그래프로 표현한다. 그래프의 간선은 통신 네트워크의 링크를 나타낸다. 본서에서 프로세서-메모리 노드는 검은 사각형으로, 스위치는 파란색 원으로 표시한다. 이 절의 링크는 모두 양방향성이다.
모든 네트워크는 스위치들로 구성되며, 스위치에는 프로세서-메모리 노드로 가는 링크와 다른 스위치로 가는 링크가 있다. 첫 번째 네트워크는 모든 노드를 순서대로 연결하는 네트워크이다.
이러한 연결 구조를 링(ring)이라 한다. 직접 연결되지 않는 노드들도 있으므로 어떤 메시지들은 중간 노드들을 거쳐서 최종 목적지에 도달하게 된다.
버스 —연결된 모든 장치들에게 정보를 브로드캐스팅할 수 있는 한 무리의 공유 신호선— 와는 달리 링은 여러 개의 전송을 동시에 수행할 수 있다.
위상의 종류가 다양하기 때문에 구별하기 위한 성능의 척도가 필요한데 보통 많이 쓰이는 것은 두 가지이다. 첫째는 총 네트워크 대역폭(network bandwidth)으로서 각 링크의 대역폭에 링크 수를 곱한 값이다.
이 척도는 최선의 경우에 얻을 수 있는 성능에 해당된다. 앞에서 본 링 네트워크의 경우 프로세서가 P개라면 한 링크의 대역폭에 P를 곱한 값이 된다. 반면에 버스의 경우는 버스 대역폭이 전체 대역폭이 된다.
이 최선의 경우 대역폭과 균형을 맞추기 위해서 최악의 경우에 성능에 근사한 또 다른 척도를 사용하는데 그것은 양분 대역폭(bisection bandwidth)이다.
양분 대역폭은 전체 네트워크를 같은 크기로 양분하여 그 경계선을 통과하는 링크의 대역폭을 모두 합한 값이다.
링의 양분 대역폭은 링크 대역폭의 두 배이고, 버스의 양분 대역폭은 버스의 대역폭과 같다. 링의 링크 속도와 버스의 속도가 같다면 최선의 경우 링이 P배 빠르지만 최악의 경우에는 두 배 밖에 빠르지 않다.
비대칭형인 네트워크도 있으므로 어떻게 두 개로 가르느냐 하는 것이 문제가 된다. 양분 대역폭은 최악의 성능을 표시하는 것이므로 가장 성능이 나쁜 분할을 택해야 한다.
즉 모든 가능한 경우를 계산해서 그 중에 가장 나쁜 값을 취해야 한다. 가장 나쁜 값을 취하는 이유는 통신 경로에서 가장 취약한 링크에 의해 병렬 프로그램의 성능이 제한되는 경우가 많기 때문이다.
링과 반대쪽 극단에 완전 연결 네트워크(fully connected network)가 있다. 완전 연결 네트워크는 모든 프로세서를 양방향 링크로 직접 연결하므로, 네트워크 전체 대역폭은 P×(P12)P \times ({P - 1 \over 2})이고 양분 대역폭은 (P2)2({P \over 2})^{2}이다.
완전 연결 네트워크의 엄청난 성능은 그만큼 비싼 가격을 지불하고 얻어지는 것이다. 그러므로 링의 가격과 완전 연결 네트워크의 성능을 절충한 여러 가지 네트워크가 제안되었다.
그중에서 어떤 네트워크를 사용하는 것이 좋은가는 그 컴퓨터에서 수행될 병렬 프로그램의 통신 특성에 따라 달라진다.
이제까지 발표된 네트워크 위상은 그 수를 헤아리기 어려울 정도로 많지만, 그중 상용 병렬 컴퓨터에서 실제로 사용된 것은 몇 가지 안 된다. 가장 많이 사용하는 것 두 가지를 그림 6.14에 보였다.
네트워크 노드마다 프로세서를 하나씩 두는 대신에 일부 노드에는 스위치만 두는 방법도 있다.
이 경우 스위치들은 프로세서-메모리-스위치 노드보다 훨씬 작기 때문에 더 촘촘하게 설치할 수 있어서 연결 거리를 단축하고 따라서 성능을 개선할 수 있다.
이런 네트워크에서는 보통 메시지가 여러 단계를 거쳐서 전달되기 때문에 다단계 네트워크(multistage network)라 부른다. 다단계 네트워크의 종류 또한 매우 다양한데, 그림 6.15에 가장 많이 쓰이는 것 두 가지를 보였다.
완전 연결 또는 크로스바 네트워크(crossbar network)라고 불리는 것은 어떤 노드 간의 통신도 한 번에 이루어준다.
오메가(Omega) 네트워크는 크로스바 네트워크보다 더 적은 하드웨어를 사용하는 대신 (2nlog2n2n \cdot \log_{2} nn2n^{2} 스위치) 통신 패턴에 따라 메시지들 간의 충돌이 발생할 수 있다.
예컨대 그림 6.15의 오메가 네트워크에서 P0에서 P6로 가는 메시지와 P1에서 P7으로 가는 메시지는 동시에 전송할 수 없다.

네트워크의 구현

네트워크를 실제로 구현하려면 지금까지 설명한 기본적인 사항 외에도 고려해야 할 점이 많이 있다.
링크 길이는 통신 속도를 빠르게 하는데 드는 비용에 영향을 미친다. 일반적으로 길이가 길수록 고속 통신에 더 많은 비용이 든다. 선이 짧아지면 필요한 구동 전력이 작아도 되므로 길이가 짧을수록 한 링크에 더 많은 전선을 할당할 수 있다. 선의 길이가 짧으면 당연히 가격도 싸진다.
또 다른 현실적인 제약은 칩이 2차원 구조이기 때문에 3차원 위상을 2차원으로 변환해야 한다는 점이다. 마지막으로 에너지도 고려를 해야 한다. 예컨대 멀티코어 칩이 단순한 격자 위상을 갖는 이유가 에너지를 고려한 결과일 수 있다. 그냥 그림으로 그려 놓았을 때 좋아 보이던 구조가 칩으로 구현해 놓으면 실용적이지 않을 수 있다는 점에 주의를 기울여야 한다.

외부세계와의 통신: 클러스터 네트워킹

(온라인 강의)

멀티프로세서 벤치마크와 성능 모델

1장에서 살펴본 바와 같이 시스템을 벤치마킹하는 것은 늘 민감한 이슈이다. 이는 어느 시스템이 더 좋은가를 명확히 드러내는 방법이기 떄문이다. (이하 생략)
잔꾀를 부리지 못하도록 하는 전형적인 규칙은 벤치마크를 변경하지 못하도록 하는 것이다. 소스 코드와 데이터 집합을 고정시키고 맞는 답은 하나로 정한다. 이 규정을 어기면 그 결과는 무효가 된다.
많은 멀티프로세서 벤치마크도 이러한 전통을 따른다. 공통적인 예외가 하나 있는데 그것은 문제의 크기를 키울 수 있도록 해서 동일한 벤치마크 프로그램을 프로세서 개수가 다른 시스템에서 수행할 수 있도록 한 것이다.
즉 많은 벤치마크는 경성 스케일링을 요구하는 대신 연성 스케일링을 허용한다. 따라서 문제 크기가 다른 프로그램과 결과를 비교할 때는 주의를 해야 한다.
그림 6.16은 여러 병렬 벤치마크를 요약한 것이다. 이들을 간략히 설명하면 다음과 같다.
Linpack은 선형대수 루틴의 집합으로서 가우스 소거법을 수행하는 루틴들이 Linpack 벤치마크를 구성한다. 3.5절 예제의 DGEMM 루틴은 Linpack 벤치마크 소스 코드의 아주 작은 부분을 발췌한 것이지만 벤치마크 실행시간의 대부분을 차지한다. Linepack은 연성 스케일링을 허용하여 사용자가 문제 크기를 임의로 선택할 수 있게 한다. (이하 생략)
SPECrate는 SPEC CPU 2006(1장 참조)과 같은 SPEC CPU 벤치마크를 기반으로 하는 처리량의 척도이다. SPECrate는 한 프로그램 실행에 대한 성능이 아니고 같은 프로그램을 여러 개 동시에 실행시켰을 때의 성능을 측정한다.
프로그램 사이에 통신이 없기 때문에 태스크 수준 병렬성을 측정하는 것으로 볼 수 있다. 프로그램 개수를 사용자가 정할 수 있으므로 이것도 연성 스케일링의 하나이다.
SPLASH와 SPLASH2(Stanford Parallel Applications for Shared Memory)는 1990년대에 Stanford 대학의 연구진들이 SPEC CPU 벤치마크 모음에 버금가는 병렬 벤치마크 모음을 만들려는 노력의 결과로 개발되었다. 이것은 커널 벤치마크와 응용 벤치마크를 다 포함하고 있으며, 고성능 컴퓨팅 분야에서 가져온 것이 많다. 이 벤치마크는 두 종류의 데이터 집합을 사용할 수 있지만 기본적으로 경성 스케일링을 요구한다.
NAS(NASA Advanced Supercomputing) 병렬 벤치마크는 1990년대에 멀티프로세서를 벤치마킹하려는 노력의 또 다른 산물이다. 이것은 계산유체역학에서 가져온 5개의 커널로 구성되어 있다. NAS는 몇 개의 데이터 집합을 정의함으로써 연성 스케일링을 허용한다. (이하 생략)
최근에 개발된 PARSEC(Princeton Application Repository for Shared Memory Computers) 벤치마크 모음은 Pthreads(POSIX threads)와 OpenMP(Open MultiProcessing)를 사용하는 멀티스레드 프로그램들로 구성되어 있다. 이것은 새로 떠오르는 계산 분야에 초점을 맞추고 있으며, 9개의 응용 프로그램과 3개의 커널로 되어 있다. 그 중 8개는 데이터 병렬성, 3개는 파이프라인 병렬성, 그리고 나머지 하나는 불규칙적인 병렬성을 이용하고 있다.
클라우드 영역에서 YCSB(Yahoo! Cloud Serving Benchmark)의 목적은 클라우드 데이터 서비스의 성능을 비교하기 위한 것이다. UCSB는 클라이언트가 새로운 데이터 서비스 —예컨대 Cassandra나 HBase를 사용하는— 를 쉽게 벤치마킹 할 수 있는 프레임 워크를 제공한다.
벤치마크에 제약을 가하는 이러한 전통의 나쁜 점은 기술 혁신이 주로 구조와 컴파일러에만 제한되게 한다는 것이다. 더 나은 자료구조, 알고리즘, 프로그래밍 언어 등은 사용할 수 없다. 왜냐하면 이러한 것들이 결과를 왜곡할 수 있기 때문이다. 어떤 시스템이 하드웨어와 컴파일러가 우수해서가 아니라 알고리즘이 좋아서 더 좋은 결과를 낼 수 도 있다.
(이하 생략)

성능 모델

벤치마크와 관련된 주제로 성능 모델이 있다. 멀티스레딩, SIMD, GPU 등 구조의 다양성이 계속 증가하고 있는 현실에서 서로 다른 설계들의 성능을 간파할 수 있는 모델이 있다면 큰 도움이 될 것이다. 모델이 완전할 필요는 없고 단지 어느 정도의 직관력을 줄 수 있으면 족하다.
5장의 캐시 성능을 위한 3C는 성능 모델의 한 예이다. 이것은 블록의 크기, 블록 할당 정책, 블록 교체 정책 등과 같이 중요할 수도 있는 요소들을 무시하였다는 점에서 완전한 모델로 볼 수 없다.
게다가 어떤 설계에서는 용량 때문에 캐시 실패가 발생 하지만 동일한 크기의 다른 캐시에서는 대립 실패가 발생하는 일관성 없는 경우도 존재한다. 그럼에도 3C 모델은 지난 25년 동안 널리 사용되어 왔다.
이것은 이 모델이 프로그램 행동을 파악할 수 있게 도와줌으로써 컴퓨터 구조 설계자들이나 프로그래머들이 자신이 만든 것을 개선할 수 있도록 하였기 때문이다.
병렬 컴퓨터를 위한 이러한 모델을 찾기 위하여 그림 6.16의 버클리(Berkeley) 설계 패턴 13개 같은 작은 커널을 먼저 살펴보자.
이 커널들은 다른 데이터 타입을 사용하는 여러 가지 버전이 있지만, 그중에서도 부동소수점이 널리 사용되고 있다. 따라서 그 컴퓨터의 최고 부동소수점 성능이 이 컴퓨터에서 수행되는 커널 속도의 한계가 된다.
멀티코어 칩의 최고 부동소수점 성능은 칩 안의 코어들이 갖고 있는 최고 성능을 모두 합한 값이다. 시스템에 여러 개의 마이크로프로세서가 있다면 칩의 최고 성능을 칩의 개수와 곱한 것이 전체 성능이다.
이 최고 부동소수점 성능을 메모리에 한 바이트 접근할 때마다 수행하는 평균 부동소수점 연산 개수로 나눈 값으로 메모리 시스템에 대한 요구를 예측할 수 있다.
부동소수점 연산(초) / 부동소수점 연산(바이트) = 바이트/초
부동소수점 연산 횟수를 메모리에서 읽거나 쓴 바이트 수로 나눈 값을 연산 강도(arithmetic intensity)라고 한다. 이것은 프로그램의 부동소수점 연산 총수를 프로그램 실행 중에 메인 메모리로 전송된 데이터의 총 바이트 수로 나누어서 구할 수 있다. 그림 6.17은 그림 6.16의 버클리 설계 패턴 중 몇 개의 연산 강도를 보여준다.

루프라인 모델

여기서 제안하는 간단한 모델은 부동소수점 성능, 연산 강도, 메모리 성능을 2차원 그래프로 엮은 것이다. 최고 부동소수점 성능은 앞에서 언급한 하드웨어 명세로부터 얻을 수 있다.
여기서 고려하는 커널의 워킹셋은 온칩 캐시보다 크기 떄문에 최고 메모리 성능은 캐시 뒤편의 메모리 시스템에 대하여 정의된다. 최대 메모리 성능을 구하는 방법의 하나는 Stream 벤치마크를 사용하는 것이다.
그림 6.18이 이 모델을 보여 주는데 이것은 각 커널에 대한 것이 아니라 컴퓨터 한 대에 대한 모델이다.
수직 Y축은 0.5부터 64. GFLOPs/초까지 표시한 부동소수점 성능이다. 수평 X축은 1/8 FLOPs/DRAM 접근 바이트로부터 16 FLOPs/DRAM 접근 바이트까지의 구간을 표시한 연산 강도이다. 그래프가 로그-로그 눈금인 것에 유의하라.
어떤 커널이 주어지면 연산 강도를 보고 X축의 한 점을 정할 수 있다. 그 점에서 수직선을 그으면 그 선 어딘가에 커널의 성능에 해당되는 점이 있을 것이다. 또 컴퓨터의 최고 부동소수점 성능에 해당하는 수평선을 그릴 수 있다. 이 선은 하드웨어의 한계를 표시하므로 실제 부동소수점 성능이 그보다 더 높을 수는 없다.
최대 메모리 성능은 어떻게 그리는가? X축이 FLOPs/바이트이고 Y축이 FLOPs/초 이기 때문에, 바이트/초는 그림에서 45도 각을 갖는 대각선이 된다. 따라서 주어진 연산 강도에서 컴퓨터의 메모리 시스템이 지원할 수 있는 최고 부동소수점 성능을 표시하는 세 번째 선을 그릴 수 있다.
그림 6.18의 그래프에 이 선을 그리기 위한 식을 다음과 같이 표현할 수 있다.
얻을 수 있는 GFLOPs/초 = Min(최대 메모리 대역폭 x 연산 강도, 최고 부동소수점 성능)
그림의 수평선과 대각선 모양 때문에 이 모델의 이름이 루프라인(roofline)이 되었으며 그 가치를 드러낸다. 루프라인은 연산 강도에 따라 정해지는 커널 성능의 상한치를 결정한다. 컴퓨터의 루프라인은 커널에 따라 변하는 것이 아니기 때문에 한번 그리면 반복해서 사용할 수 있다.
연산 강도를 지붕까지 다다른 장대로 생각하면 장대가 지붕의 평평한 부분에 닿는 경우는 성느잉 계산에 의해 제한을 받는 것을 의미하며, 장대가 지붕의 경사진 부분에 닿는다면 메모리 대역폭에 의해 성능이 제한된다는 것을 의미한다. 그림 6.18에서 커널 2는 전자의 예이며, 커널 1은 후자의 예이다.
수평 지붕선과 대각선이 만나는 마룻점(ridge point)은 컴퓨터에 대한 흥미로운 점을 시사한다. 이 점이 오른쪽으로 치우친다면 연산 강도가 매우 큰 커널만 컴퓨터의 최고 성능을 향유할 수 있을 것이다. 반면에 이 점이 왼쪽에 있다면 거의 모든 커널이 최고 성능을 누릴 것이다.
이 두 가지 경우의 예를 곧 살펴보게 될 것이다.

두 세대 Opteron의 비교

4개의 코어를 가지는 AMD 사의 Opteron X4(Barcelona)는 2개의 코어를 가지는 Opteron X2의 후속 제품이다. 보드 설계를 간단하게 하기 위해 같은 소켓을 사용하였다. 따라서 이 둘은 동일한 DRAM 채널을 가지고 동일한 최대 메모리 대역폭을 갖는다.
Opteron X4는 코어의 수를 2배로 증가시켰을 뿐만 아니라 코어당 부동소수점 성능을 2배로 개선하였다. Opteron X4는 매 클럭 사이클당 두 개의 부동소수점 SSE2 명령어를 내보낼 수 있다.
반면에 Opteron X2는 기껏해야 한 개를 내보낼 수 있을 뿐이다. Opteron X2는 2.2 GHz, Opteron X4는 2.3 GHz로 비슷한 클럭 속도를 갖기 때문에 DRAM 대역폭이 같다면 최고 부동소수점 성능은 Opteron X4가 Opteron X2보다 4배 더 높다. 또 Opteron X4는 Opteron X2에는 없는 2MiB의 L3 캐시가 있다.
그림 6.19는 두 시스템의 루프라인 모델을 비교한 것이다. 예상할 수 있듯이 Opteron X2의 마룻점은 1인데 Opteron X4의 마룻점은 5로 이동하였다. 따라서 Opteron X4가 X2보다 더 좋은 성능을 내기 위해서는 프로그램의 연산 강도가 1보다 크거나 Opteron X4의 캐시보다 워킹셋이 작아야 한다.
루프라인 모델은 성능의 상한치를 제시한다. 만약 당신의 프로그램이 이 상한치보다 매우 낮은 성능을 보인다면 어떤 최적화를 어떤 순서로 진행해야 할까? 다음 두 가지 최적화는 거의 모든 커널의 계산 병목을 줄이는데 도움이 된다.
1.
부동소수점 연산의 혼합
컴퓨터의 최고 부동소수점 성능을 얻기 위해서는 일반적으로 동일한 수의 덧셈과 곱셈을 거의 동시에 수행하는 것이 필요하다. 이러한 균형이 필요한 이유는 컴퓨터가 곱셈-덧셈 융합(fused multiply-add) 명령어를 지원하거나 부동소수점 유닛이 같은 수의 부동소수점 덧셈기와 부동소수점 곱셈기를 가지고 있기 때문이다.
또한 최고 성능을 얻기 위해서는 명령어의 대부분이 부동소수점 연산과 비정수 연산 명령어가 되도록 해야 한다.
2.
명령어 수준 병렬성의 개선 및 SIMD의 적용
최신 구조에서는 매 클럭 사이클당 3 또는 4개의 명령어를 인출, 실행, 완료할 때 최고 성능을 얻게 된다. (4.10절 참조) 이를 위해서는 컴파일러가 생성하는 코드의 ILP가 증가하도록 코드를 개선하는 것이 중요하다.
한 가지 방법은 4.12절에서 살펴본 바와 같이 순환문 펼치기를 하는 것이다. x86 구조에서 AVX 명령어 하나는 2배 정밀도 피연산자 네 개를 연산할 수 있으므로 가능한 한 이런 명령어를 많이 사용하도록 한다.
메모리 병목을 줄이기 위해서는 다음 두 가지 최적화가 도움이 된다.
1.
소프트웨어 선인출
성능이 최고 수준이 되면 많은 메모리 작업들이 계속 진행될 필요가 있다. 이렇게 하려면 데이터가 필요할 때까지 기다리지 말고 소프트웨에 선인출 명령을 이용한 예측 접근(predicting access)을 수행하는 것이 좋다.
2.
멤뢰 친화도(memory affinity)
오늘날 대부분의 마이크로프로세서는 마이크로프로세서와 같은 칩에 메모리 제어기를 포함하고 있으며 이를 통해 메모리 계층의 성능이 개선된다.
시스템이 여러 칩으로 구성되어 있는 경우 어떤 주소들은 같은 칩에 있는 DRAM으로 가겠지만 나머지 다른 주소들은 칩 간 연결선을 통해서 다른 칩의 DRAM으로 보내져야 한다. 이렇게 나누어지는 것이 6.5절에서 설명한 비균일 메모리 접근을 유발하고 다른 칩에 있는 메모리에 접근하는 것이 성능을 떨어뜨린다.
이 두 번째 최적화 기법은 프로세서가 다른 칩의 메모리에 접근하는 일이 거의 발생하지 않도록 데이터와 그 데이터를 사용하는 스레드를 같은 메모리-프로세서 쌍에 할당하고자 하는 것이다.
루프라인 모델은 이러한 최적화들 중에서 어떤 것들을 어떤 순서로 적용할 것인지를 결정하는데 도움을 준다. 앞의 각 최적화 기법들은 루프라인 아래에 있는 어떤 '상한선(ciling)'과 연관 지어 생각할 수 있는데, 해당 최적화를 실행하지 않고는 이 상한선을 뚫고 나갈 수 없다.
계산 루프라인은 설명서로부터 얻을 수 있고, 메모리 루프라인은 Stream 벤치마크를 돌려서 얻을 수 있다. 부동소수점 균형과 같은 계산 상한선도 컴퓨터의 설명서로부터 얻을 수 있다.
메모리 친화도와 같은 메모리 상한선을 알기 위해서는 각 컴퓨터에서 실험하여 그 차이를 측정해야 한다. 다행히도 이와 같은 과정은 컴퓨터마다 한 번씩만 수행하면 된다.
그러므로 일단 누군가가 어떤 컴퓨터의 상한성 특성을 파악하면 다른 모든 사람들은 이를 이용하여 그 컴퓨터에서 수행할 최적화의 우선순위를 결정할 수 있다.
그림 6.20은 그림 6.18의 루프라인 모델에 상한선을 추가한 것이다. 위의 그래프는 계산 상한선을 보여주고 아래 그래프는 메모리 대역폭 상한선을 보여 주고 있다. 위쪽 상한선에 두 최적화의 이름을 다 쓰지는 않았지만, 위쪽 상한선을 돌파하려면 당연히 그 아래의 상한선을 통과해야 한다.
상한선과 그 다음 상한선 간의 간격은 이 최적화를 수행함으로써 얻는 이득을 의미한다. 그림 6.20을 보면 이 컴퓨터에서는 ILP를 개선하는 2번 최적화가 계산 성능을 더 크게 개선하며 메모리 친화도를 개선하는 4번 최적화가 메모리 대역폭을 더 크게 개선함을 알 수 있다.
그림 6.21은 그림 6.20의 상한선들을 모아서 하나의 그래프로 만든 것이다. 커널의 연산 강도가 최적화 영역을 결정하고, 그것이 어떤 최적화를 수행할지를 알려준다. 계산 최적화와 메모리 대역폭 최적화가 중첩되는 연산 강도가 많음에 유의하라.
그림 6.21에서 다른 최적화 전략이 필요한 세 영역을 구별하여 음영 처리하였다. 예컨대 커널 2는 오른쪽의 파란 사다리꼴 영역에 해당되기 때문에 계산 최적화만을 수행하면 된다. 하지만 커널 1은 가운데의 파란색과 회색이 겹쳐진 평행사변형 영역에 위치하므로 두 가지 최적화를 다 수행해야 한다.
그뿐만 아니라 최적화 2번과 4번을 먼저 하는 것이 좋음을 알 수 있다. 커널 1의 수직선이 부동소수점 불균형 최적화의 아래에 위치하기 때문에 1번 최적화는 필요 없는 것으로 판단된다. 만약 커널이 왼편 아래쪽에 있는 회색 삼각형 영역에 들어 있다면 메모리 최적화만 필요한 것으로 판단할 수 있다.
지금까지는 연산 강도가 고정되어 있는 것을 가정하였으나 실제로 그렇지 않을 수도 있다.
첫째, 밀집행렬이나 N-body 문제와 같이(그림 6.17 참조) 문제 크기가 커짐에 따라 연산 강도가 커지는 커널들도 있다. 실제로 이 때문에 경성 스케일링보다 연성 스케일링을 통해서 더 큰 성과를 거둘 수 있었다.
둘째, 메모리 계층구조의 효율성이 메모리에 접근하는 빈도수에 영향을 끼치기 때문에 캐시 성능을 높임으로 해서 연산 강도를 개선할 수 있다. 순환문 펼치기를 수행한 후에 비슷한 주소의 문장들을 묶어서 시간적 지역성을 높이는 것이 한 예이다.
캐시에 데이터가 들어갈 자리는 할당하지만 곧 덮어 쓰일 것이기 때문에 메모리에서 데이터의 초깃값을 읽어 오지는 않도록 하는 특별한 캐시 명령어를 가지고 있는 컴퓨터들이 많이 있다. 이 두 가지 최적화는 메모리 통신량을 줄여서 연산 강도 장대를 오른쪽 일정 비율(예컨대 1.5) 만큼 이동시킨다. 이렇게 오른쪽으로 이동시키면 커널이 다른 최적화 영역에 들어갈 수도 있다.
앞에서는 어떻게 프로그래머가 성능을 개선할 수 있을지를 살펴보았지만, 루프라인 모델은 하드웨어 설계자가 중요하다고 생각하는 커널의 성능을 높이기 위해 하드웨어의 어떤 부분을 최적화하는 것이 좋은지를 결정하는데도 사용할 수 있다.

실례: Intel Core i7 960과 NVIDIA Telsa GPU의 벤치마킹과 루프라인

Intel 사 연구진들이 최근에 멀티미디어 SIMD 확장을 장착한 쿼드코어 Intel Core i7 960과 한 세대 전 GPU인 NVIDIA Tesla GPU 280의 성능을 비교한 논문을 발표하였다. 두 시스템의 특성을 그림 6.22에 나열하였다. 두 제품 모두 2009년 가을에 구매한 것이다. Core i7은 Intel의 45nm 공정을 사용하였고, GPU는 TSMC의 65 nm 공정을 사용하였다.
그림 6.23의 루프라인은 Core i7 960과 GTX 280의 차이점을 보여 준다. GTX가 훨씬 큰 메모리 대역폭과 2배 정밀도 부동소수점 성능을 가지고 있을 뿐 아니라 2배 정밀도 마룻점이 훨씬 왼쪽에 위치한다.
2배 정밀도 마룻점이 GTX 280의 경우에는 0.6이지만 Core i7의 경우에는 3.1이다. 앞서 설명한 바와 같이 마룻점이 왼쪽으로 갈수록 최고 계산 성능에 도달하는 것이 쉽다. 단일 정밀도 성능의 경우 두 컴퓨터 모두 마룻점이 우측으로 옮겨져서 단일 정밀도 성능의 상한치에 도달하는 것이 훨씬 어렵다.
커널의 연산 강도는 캐시 메모리로 가는 바이트가 아니라 메인 메모리로 가는 바이트에 의해 결정되는 것에 유의하라. 앞서 설명한 바와 같이 대부분의 메모리 참조가 캐시에서 이루어진다면 캐싱은 특정 컴퓨터에 대한 커널의 연산 강도를 변화시킨다.
또한 두 컴퓨터 모두 메모리 접근이 순차적으로 이루어지는 경우의 대역폭을 가정하고 있음에 유의할 필요가 있다. 곧 살펴보겠지만 수집-분산 방식의 주소 접근은 GTX 270과 Core i7에서 실제로 느리게 수행된다.
연구진들은 최근에 제안된 벤치마크 모음 네 가지의 계산 특성과 메모리 특성을 분석하여 벤치마크 프로그램을 선정하고 '이들 특성을 담아내는 처리량 계산 커널(throughput computing kernel) 집합을 구성'하였다. 그림 6.24는 성능 측정 결과를 보여주는데, 큰 숫자가 빠른 것을 의미한다. 루프라인은 이 사례연구의 상대적 성능을 설명하는데 도움이 된다.
성능 자료를 바탕으로 GTX 280의 성능을 Core i7과 비교하면 2.5배 느리거나(클럭 속도의 경우), 7.5배 빠를 수 있는 (칩당 코어 수의 경우) 것처럼 보이지만, 실제 성능은 2.0배 느리거나(Solv) 15.2배 빠른 (GJK) 범위에 있는 것으로 측정되었으며 Intel 연구진들은 그렇게 차이가 나는 이유를 다음과 같이 분석하였다.
메모리 대역폭 (상세 설명 생략)
계산 대역폭 (상세 설명 생략)
캐시 이득 (상세 설명 생략)
수집-분산 (상세 설명 생략)
동기화 (상세 설명 생략)
(이하 설명 생략)

더 빠르게: 복수의 프로세서와 행렬 곱셈

Intel Core i7(Sandy Bridge) 하드웨어에 적합하도록 DGEMM를 변환하여 점진적으로 성능을 개선시키는 여정의 마지막이면서 가장 효과가 큰 단계를 설명한다. 우리가 사용하는 컴퓨터는 2개의 Core i7을 가지고 있는데, 각 Core i7은 8개의 코어를 가지고 있다. 따라서 우리는 16개의 코어를 사용하여 DGEMM을 수행한다.
아래 코드는 이러한 멀티코어를 활용해서 DGEMM을 실행하는 OpenMP 버전의 코드를 보여준다. 30번째 줄은 그림 5.48에 단 한개의 줄이 더 추가된 것인데, 이 줄로 인하여 코드가 복수의 프로세서에서 수행될 수 있다.
이 줄은 컴파일러로 하여금 최외곽 순환문을 다수의 스레드를 이용하여 수행하도록 지시하는 OpenMP pragma이다. 이 pragma는 컴퓨터로 하여금 최외곽 순환문을 모든 스레드로 분할하여 수행하게끔 한다.
#include <x86intrin.h> #define UNROLL(4) #define BLOCKSIZE 32 void do_block(int n, int si, int sj, int sk, double *A, double *B, double *C) { for (int i = si; i < si+BLOCKSIZE; i+=UNROLL*4) { for (int j = sj; j < sj+BLOCKSIZE; j++) { __m256d c[4]; for (int x = 0; x < UNROLL; x++) { c[x] = _mm256_load_pd(C+i+x*4+j*n); /* c[x] = C[i][j] */ } for (int k = sk; k < sk+BLOCKSIZE; k++) { __m256d b = _mm256_broadcast_sd(B+k+j*n); /* b = B[k][j] */ for (int x = 0; x < UNROLL; x++) { c[x] = _mm256_add_pd(c[x], /* c[x] += A[i][k]*b */ _mm256_mul_pd(_mm256_load_pd(A+n*k+x*4+i), b)); } } for (int x = 0; x < UNROLL; x++) { _mm256_store_pd(C+i+x*4+j*n, c[x]); /* C[i][j] = c[x] */ } } } } void dgemm(int n, double* A, double* B, double* C) { #pragram omp parallel for for (int sj = 0; sj < n; sj += BLOCKSIZE) { for (int si = 0; si < n; si += BLOCKSIZE) { for (int sk = 0; sk < n; sk += BLOCKSIZE) { do_block(n, si, sj, sk, A, B, C); } } } }
C++
그림 6.26은 단일 스레드에 비해서 스레드의 개수가 증가함에 따라 성능이 어떻게 개선되는지를 보여주는 전통적인 멀티프로세서 속도 개선 그래프를 도시하고 있다.
이 그래프는 연선 스케일링에 비해 경성 스케일링이 더 어려움을 분명히 드러낸다. 32 x 32 행렬의 경우와 같이 모든 것이 1차 데이터 캐시에 더 적재된다면 스레드 개수를 늘리는 것이 실제로 성능을 저해한다. 이 경우 16개의 스레드를 이용한 DGEMM 버전이 단일 스레드 버전에 비하여 반 정도의 성능을 보인다.
이와 반대로 2개의 가장 큰 행렬에 대해서는 16개의 스레드를 사용한 버전이 14배의 성능 개선을 보이는데 그림 6.26에서 전통적으로 '우상향'으로 증가하는 선에 해당된다.
그림 6.27은 스레드의 개수가 1에서 16으로 증가될 때 성능의 절대치가 어떻게 변하는지를 보여준다. DGEMM은 960 x 960 행렬의 경우 174 GFLOPS의 성능으로 동작한다.
그림 3.21에서 최적화되지 않은 DGEMM의 C 코드의 성능은 겨우 0.8 GLOPS이었으므로 3장에서 6장까지의 최적화를 통해 주어진 하드웨어에서 200배 이상의 속도 개선을 얻게 된 것이다.

오류 및 함정

오류: 병렬 컴퓨터에서 Amdahl의 법칙은 적요오디지 않는다.
1987년 한 연구소 소장이 어떤 멀티프로세서로 Amdahl의 법칙을 깨뜨렸다고 주장하였다. 이 언론 기사가 어떤 기반 위에 이루어진 것인지에 대한 이해를 돕기 위해 Amdahl의 법칙 원전을 인용한다.
이 시점에서 명백한 결론은 높은 병렬 처리율을 얻기 위한 노력은 같은 정도의 순차 처리 성능 개선이 동반되지 않는 한 무위에 그칠 것이라는 사실이다.
이 진술은 아직도 유효하다. 즉 무시된 프로그램 부분이 궁극적인 성능을 제한한다. 이 법칙에서 다음과 같은 보조정리를 도출할 수 있다.
어떤 프로그램이든지 순차적인 부분이 있기 마련이고, 따라서 경제적인 측면에서 볼 때 프로세서 개수의 상한선(일단 100개라고 가정하자)이 존재한다. 이에 반해 프로세서 1,000개까지 선형으로 속도가 개선됨을 보여서, 이 보조정리가 틀렸음을 증명하고 Amdahl의 법칙이 깨졌다고 주장한 것이다.
이 사람은 단지 연성 스케일링 방식을 사용하였을 뿐이다. 즉 같은 데이터 집합에 대해 1,000배 빨라진 것이 아니라, 같은 시간에 1,000배 많은 일을 처리한 것이다.
여기서 사용된 알고리즘에서 순차적 부분은 입력의 크기에 관계없이 일정한 비율이었으며 나머지 부분은 완전히 병렬로 처리되었다. 따라서 프로세서가 1,000개라도 선형적인 속도 개선이 가능하였던 것이다.
Amdahl의 법칙은 병렬 프로세서에 분명히 적용된다. 이 연구가 지적한 사실은 빠른 컴퓨터를 사용하는 주된 용도의 하나가 더 큰 크기의 문제를 수행하는 것이라는 점이다. 사용자는 많은 수의 프로세서를 바쁘게 만드는 문제 하나를 발견해서 비싼 컴퓨터를 구입하는 것을 정당화하려는 노력을 하는 대신에 더 큰 문제들에 관심을 기울여야 함을 명심하자.
오류: 최고 성능은 실제 성능을 반영한다.
수퍼컴퓨터 업계에서는 이 수치를 마케팅에 사용하고 있으며, 병렬 컴퓨터의 경우에는 그 정도가 더 심하다. 단일 프로세서 노드의 최고 성능을 사용하는 선에서 그치지 않고, 완벽한 성능 개선을 가정하고 그것을 전체 프로세서 개수와 곱해서 최고 성능이라고 주장한다.
Amdahl의 법칙은 둘 중 하나의 최고치에 도달하는 것만도 얼마나 어려운 일인가를 말해주고 있는데, 이 둘을 곱하다니 이것은 그들이 저지르는 죄의 크기를 곱하는 일이다. 루프라인 모델은 최고 성능을 어떻게 이해할 것인지를 도와준다.
함정: 멀티프로세서 구조를 이용하는 혹은 멀티프로세서에 최적화된 소프트웨어를 개발하지 않는 것
병렬 소프트웨어가 병렬 프로세서보다 뒤쳐진다는 것은 오래된 이야기인데 소프트웨어 문제가 훨씬 어렵다는 것이 이유가 될 수 있다. 한 예를 가지고 이 문제가 얼마나 미묘한지를 보이려고 한다. 이런 예는 수 없이 많다.
종종 직면하는 문제는 단일 프로세서를 위해 개발된 소프트웨어를 멀티프로세서용으로 변경할 때 발생한다. 예컨대 SGI 운영체제는 페이지 할당이 드물게 일어난다는 것을 가정하고 한 개의 잠금 변수를 사용하여 페이지 테이블을 보호하도록 처음에 설계되었다.
단일 프로세서에서는 이로 인한 성능상의 문제가 없었다. 하지만 멀티프로세서에서는 이것이 어떤 프로그램에 대해 심각한 성능 병목이 되었다. 시동할 때에 초기화되는 페이지의 숫자가 많은 프로그램이 있다고 하자. UNIX는 정적으로 할당되는 페이지를 이런 방식으로 초기화 한다.
이 프로그램이 병렬화되어 여러 프로세스가 페이지를 할당한다고 가정하자. 페이지를 할당하려면 페이지 테이블을 사용해야 하는데, 페이지 테이블은 사용 중일 때마다 잠겨 있게 될 것이다.
그러므로 모든 프로세스가 페이지들을 동시에 할당하려고 하면 운영체제 커널은 멀티스레드를 지원한다고 하여도 어쩔 수 없이 직렬적으로 수행할 수 밖에 없다. 우리가 예상할 수 있는 바와 같이 프로그램을 시동할 때에 이 문제가 바로 발생할 것이다.
이와 같은 페이지 테이블 직렬화는 초기화 과정의 병렬성을 없애서 전체 병렬 성능에 큰 영향을 미치게 된다. 이와 같은 성능의 병목은 태스크 수준 병렬성에도 존재한다.
예컨대 병렬처리 프로그램을 작업들 사이에 공유되는 것이 하나도 없는 개별적인 작업으로 분할하고 각 프로세서가 한 작업씩 수행하도록 한다고 하자. 불행히도 잠금변수는 여전히 모든 작업들을 직렬화한다. 따라서 독립적인 작업의 성능도 좋지 않다.
이 함정은 소프트웨어를 멀티프로세서에 수행시킬 때 발생할 수 있는 미묘하면서도 중대한 성능상의 결함을 보여준다. 다른 많은 주요 소프트웨어 구성 요소들처럼 운영체제, 알고리즘과 자료구조도 멀티프로세서 환경에서 재검토되어야 한다. 페이지 테이블을 작은 부분으로 나누고 각 부분마다 잠금변수를 두도록 하여 앞의 문제를 효과적으로 해결하였다.
오류: 메모리 대역폭을 제공하지 않고도 좋은 벡터 성능을 얻을 수 있다.
루프라인 모델에서 살펴본 바와 같이 메모리 대역폭은 모든 구조에서 상당히 중요하다. DAXPY는 부동소수점 연산 하나당 1.5번의 메모리 참조를 요구하는데, 이 수치는 많은 과학 계산용 코드에 일반적으로 해당된다.
부동소수점 연산을 수행하는데 시간이 전혀 걸리지 않는다고 하여도 Cray-1은 벡터열의 성능을 증가시킬 수 없는데 이는 성능이 메모리에 의하여 제한되기 때문이다. Cray-1은 컴파일러가 블로킹을 이용하여 계산 값이 모두 벡터 레지스터에 남도록 변경함으로써 Linpack의 성능을 한 단계 도약시킬 수 있었다.
이 방법을 사용하여 FLOP당 메모리 접근 빈도수를 줄임으로써 거의 2배의 성능 개선을 얻을 수 있었다. 이 방법을 통하여 전에 더 큰 대역폭을 요구했던 순환문을 Cray-1의 메모리 대역폭으로 충분하게 바꾼 것이며 이것이 바로 루프라인 모델이 예상한 바이다.

결론

프로세서를 단순히 모아서 컴퓨터를 구성하려는 꿈은 컴퓨터의 초창기부터 항상 있었다. 그러나 효과적이면서 효율적으로 병렬 프로세서를 구성하고 사용하는 것은 느리게 진척되었다.
유용성과 효율성을 개선하기 위하여 멀티프로세서 구조가 오랜 시간 진화한 것과 더불어 어려운 소프트웨어 문제가 발전의 속도를 제한한 것이다. 이 장에서 우리는 Amdahl의 법칙 때문에 좋은 속도 개선을 얻는 프로그램을 작성하기 어렵다는 점을 비롯하여 많은 소프트웨어의 어려움들을 살펴보았다.
아주 다양한 방식의 구조가 존재한다는 점과 과거의 많은 병렬 구조들이 오래지 않아 사라졌다는 사실이 소프트웨어의 어려움을 가중시켜 왔다. 온라인 사이트에 있는 6.15절에서 이런 멀티프로세서의 역사에 대해 설명한다.
1장에서 살펴본 바와 같이 이런 가지각색이면서도 오랜 역사에도 불구하고 IT 업계는 자신들의 미래가 병렬 컴퓨팅에 달려있다고 생각하고 있다. 과거의 많은 경우처럼 이러한 노력들이 쉽게 실패로 돌아갈 수 있음에도 불구하고 우리가 희망을 가질 이유들이 있다.
서비스로서의 소프트웨어(SaaS)의 중요성이 증가하고 있으며 이러한 서비스를 제공해 주는데 클러스터가 매우 성공적인 방법이라는 것이 증명되었다. (이하 생략)
우리는 창고 규모의 컴퓨터가 서버를 설계하는 목표와 원칙들을 바꾸고 있다고 믿는데 이것은 휴대용 클라이언트가 마이크로프로세서를 설계하는 목표와 원칙을 바꾸고 있는 것과 마찬가지다. 또한 이 둘은 소프트웨어 산업에 혁명을 일으키고 있다. (이하 생략)
SIMD와 벡터 연산은 포스트 PC 시대에 큰 역할을 담당하고 있는 멀티미디어 응용과 잘 어울린다. 이들은 전통적인 병렬 MIMD 프로그래밍보다 더 쉽다는 이점이 있고 에너지 측면에서도 MIMD 보다 더 효율적이다.
그림 6.28은 시간에 따라 MIMD에서의 코어의 수와 x86 컴퓨터의 SIMD 모드에서 클럭 사이클당 32비트와 64비트 연ㅅ나이 수행되는 횟수가 어떻게 변화되고 있는지를 도시하고 있다. x86 컴퓨터의 경우 칩당 코어 개수가 2년마다 2배씩 증가하고, SIMD 폭은 4년마다 두 배씩 증가할 것으로 예상된다. (이하 생략)
과학이나 공학 계산 분야에서 병렬처리를 사용하는 것이 점차 흔해지고 있다. 이러한 응용 분야에서 계산량에 대한 요구는 한이 없다. 또한 자연스럽게 병행성을 가지는 많은 응용이 존재한다. 이 응용 분야에서도 클러스터가 가장 널리 쓰인다. (이하 생략)
모든 데스크톱이나 서버 마이크로프로세서 제작사들은 고성능을 얻기 위해 멀티프로세서를 만들고 있다. 과거와 달리 순차 응용들로부터 고성능을 얻는 쉬운 길이 없다. 앞서 말한 바와 같이 순차적인 프로그램은 오늘날 느린 프로그램이다. 그러므로 고성능을 얻고자 하는 프로그래머들은 코드를 병렬화하든지, 아니면 병렬처리 프로그램을 새로 작성해야 한다.
과거에 마이크로프로세서와 멀티프로세서는 성공에 대한 정의를 서로 다르게 하였다. 단일 프로세서 성능의 확장성을 이야기할 때에 마이크로프로세서 설계자들은 단일 스레드의 성능이 증가된 실리콘 면적의 제곱근에 비례하여 증가하는 것으로 만족하였다. 그래서 그들은 그들은 사용하는 자원에 대한 준선형(subliear)적인 성능 개선에 만족하였다.
멀티프로세서의 경우는 프로세서 개수에 선형적으로 비례하는 속도 개선을 성공으로 정의하였다. 이는 n개의 프로세서를 구매하거나 유지하는 비용이 프로세서 하나의 n배라고 가정한 것이다. 이제 멀티코어를 통해 칩 안에서 병렬성이 구현되기 시작하였으므로 전통적인 마이크로프로세서의 성공에 대한 정의를 따라 준선형적인 성능 개선을 얻는 것을 성공으로 정의할 수 있다.
JIT(just-in-time) 실시간 컴파일 기술과 오토튜닝(autotuning) 기술이 성공적으로 개발되고 있으므로 향후 소프트웨어가 칩 내 코어의 수가 증가하는 것을 적응적으로 이용하는 것이 가능해질 것으로 생각된다. 이 기술은 정적 컴파일러를 사용할 때에는 얻을 수 없는 유연성을 제공한다.
과거와 달리 공개 소프트웨어 운동은 소프트웨어 산업계에 중요한 부분을 차지하고 있다. 이 운동은 능력 중심의 문화를 만들어서 더 뛰어난 공학적인 해법들이 과거의 전통보다 개발자들의 마음을 사로잡을 것이다. 이것은 과거의 소프트웨어를 버리고 새로운 언어와 소프트웨어 제품을 수용하는 혁신을 받아들이게 한다. 이와 같은 열린 문화는 신속한 변화가 일어나는 오늘날 매우 도움이 된다.