Search Results for 'parallel programming'

2 POSTS

  1. 2008.06.30 [dW] Unleashing the power of the Cell Broadband Engine
  2. 2008.03.26 멀티 코어 프로그래밍과 헤스켈 2
올해 쿼드 코어(코어 4개)를 시작으로 2년마다 코어 수가 2배씩 늘어가는 멀티코어 시대를 맞이했습니다. 과거 슈퍼 컴퓨터를 만들던 HPC(High Performance Computing) 분야 외에도 멀티코어 시대의 병렬 프로그래밍(Parallel Programming) 대한 아무런 준비가 없기 때문에 소프트웨어 개발자들은 당분간 혼돈의 시기를 살아갈 가능성이 큽니다.

하지만 대중적으로 사용되는 프로세서 중에도 이미 멀티코어 프로세서가 있는데 그 대표주자가 Cell Broadband Engine (CBE) Processor(줄여서 Cell 프로세서)입니다. PC에서는 아직 쿼드 코어도 일반화되지 않았지만 Cell 프로세서는 무려 9개의 코어를 가지고 있습니다. Cell 프로세서 프로그래밍이 어렵다고 소문이 나 있는 것도 이 때문입니다.

인텔의 코어 2 쿼드 프로세서는 4개의 동일한 코어를 가진 반면 Cell은 1개의 PPE(PowerPC Processing Element)와 8개의 SPE(Synergistic Processing Element)로 이루어져 있습니다. PPE는 PowerPC 아키텍처를 가진 메인 프로세서이고 8개의 SPE는 보조 프로세서 역할을 합니다. SPE는 각각 코드와 데이터를 저장할 수 있는 256kB의 로컬 스토어와 128비트의 레지스터를 128개 가지고 있습니다. SPE는 SIMD를 수행하는 데 최적화된 프로세서라고 보시면 됩니다.

Cell 프로그래밍은 크게 2가지 이유로 어렵습니다.

1) 메모리 모델

SPE 프로그래밍을 어렵게 하는 가장 큰 요인 중 하나는 캐쉬가 전혀 없다는 사실입니다. 로컬 스토어에 데이터를 load/store하기 위해서는 명시적으로 명령을 내려야만 합니다.

2) 병렬성

Cell 프로세서의 병렬성은 작게는 PPE와 SPE 모두 SIMD 명령을 지원한다는 점에서 나옵니다. 또한 슈퍼스칼라(superscalar)를 지원하기 때문에 한 클럭의 2개의 명령을 실행할 수 있습니다. 크게는 PPE와 8개의 SPE가 병렬로 동작합니다. PPE는 2개의 하드웨어 쓰레드를 가지고 있기 때문에 총 10개의 태스크가 동시에 수행되는 셈입니다.


멀티코어 프로그래밍에서 제일 중요한 것은 "프로그래밍 모델"입니다. 여러 코어를 효율적으로 활용할 수 있는 프로그래밍 모델을 찾는 것이 가장 큰 과제인 셈이죠. IBM dW의 Unleashing the power of the Cell Broadband Engine은 Cell 프로세서 특징과 프로그래밍 모델을 요약하고 있습니다. 어떻게 SPE를 프로그래밍해야 하는지 dW 기사가 어느 정도 이야기를 하고 있지만 쉬워 보이지는 않습니다. PPE, SPE의 특성, 메모리 모델 등을 다 고려해야만 성능이 난다고 하면 제대로 프로그래밍할 수 있는 사람은 별로 없을 테니깐요.

멀티 코어 프로그래밍과 헤스켈

Posted 2008. 3. 26. 02:01
멀티코어 시대가 오면서 병렬 프로그래밍이 성능 향상의 중요 방법으로 떠오르고 있습니다. 하지만 실제 개발 환경에서 개발자가 멀티 코어를 효율적으로 활용하기는 쉽지 않습니다.

다음은 0에서 40까지 피보나치 수열을 계산하는 간단한 C 프로그램입니다.

long long fib(long long n) {
  if (n < 2) {
    return 1;
  }
  return fib(n - 2) + fib(n - 1);
}

int main(int argc, char ** argv) {
  long long n = 0;

  for (n = 0; n <= 40; n++) {
      printf("fib(%lld) = %lld\n", n, fib(n));
  }

  return 0;
}

좀 더 효율적으로 짤 수도 있겠지만 계산량이 많은 프로그램의 예를 들기 위해 의도적으로 비효율적인 방식으로 작성했습니다. 이 프로그램을 GCC에서 최적화 옵션을 최고로 주고 컴파일한 후 실행하면 제 컴퓨터(1.66GHZ 코어 2 듀오 1GB 램)에서 대략 12.94초가 걸렸습니다.

gcc -O3 fib.c -o fib.exe
$ time ./fib.exe
real    0m12.940s
user    0m12.921s
sys     0m0.015s


같은 프로그램을 헤스켈로 작성하면 다음과 같습니다.

import Control.Monad
import Text.Printf

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

main = forM_ [0..40] $ \i ->
        printf "fib(%d) = %d\n" i (fib i)


ghc -O2 fib.hs -o fib.exe --make
$ time ./fib.exe
real    0m12.454s
user    0m0.031s
sys     0m0.015s


위 프로그램을 컴파일해서 실행하면 12.45초가 걸립니다. 약간이긴 하지만 오히려 C 보다 빠릅니다. 성능을 논하기에는 너무 간단한 예이긴 하지만 함수형 언어가 느리다고 생각하는 것과는 상반된 결과입니다.


위 두 예제는 멀티코어를 전혀 활용하지 못하는 프로그램입니다. fib.exe 실행시켜 놓고 윈도 작업 관리자로 확인해보면 CPU 점유율이 50%에 머물고 있는 것을 볼 수 있습니다. 두 개의 코어 중에서 하나만 죽어라고 쓰고, 나머지 하나는 놀고 있기 때문입니다. 이런 식이면 1.66GHZ 짜리 코어를 10개를 갖다 붙여도 위 프로그램은 성능은 전혀 개선되지 않게 되겠죠.

소프트웨어 성격에 따라 차이는 있겠지만 과거와 달리 멀티 코어 시대에는 소프트웨어 성능 향상을 위해 개발자의 명시적인 개입이 필요하다는 이야기가 여기서 나옵니다.

C로 작성된 프로그램을 멀티 코어를 이용하도록 개선하기 위해서는 pthread 같은 쓰레드 라이브러리를 이용해 명시적으로 쓰레드를 만들고 일을 쪼개서 데이터를 처리하고 공유 메모리를 통해 동기화를 해야할 것입니다. 간단한 예제이지만 제대로 짜기가 무척 어려우므로 실제로 코드를 작성하지 않고 넘어가겠습니다.


대신에 헤스켈은 순차적으로 작성된 프로그램을 병렬화하는 것이 상대적으로 매우 쉽습니다. fib2.hs는 fib.hs에 병렬화(parallelism)을 위한 어노테이션(`par`와 `seq`)을 덧붙인 코드입니다.

import Control.Parallel
import Control.Monad
import Text.Printf

cutoff = 30

fib' :: Int -> Integer
fib' 0 = 0
fib' 1 = 1
fib' n = fib' (n-1) + fib' (n-2)

fib :: Int -> Integer
fib n | n < cutoff = fib' n
      | otherwise  = r `par` (l `pseq` l + r)
 where
    l = fib (n-1)
    r = fib (n-2)

main = forM_ [0..40] $ \i ->
            printf "fib(%d) = %d\n" i (fib i)


헤스켈이 병렬 프로그래밍을 하는 방법에는 Control.Concurrent 모듈을 이용해 명시적으로 쓰레드를 생성하고 MVar나 STM을 이용해 공유 메모리를 제어하는 방법도 있지만, 위 예제처럼 Control.Parallel 모듈의 par와 seq 함수를 이용하는 방법도 있습니다.

위 예제는 특정 cutoff보다 작은 숫자는 쓰레드를 생성하지 않고 직접 계산을 하되, cutoff보다 크면 쓰레드를 만들라는 힌트를 줍니다. a `par` b는 a를 계산(evaluation)할 때 CPU가 놀고 있으면 쓰레드를 만들어서 작업하라고 런타임에 힌트를 줍니다. a `seq` b는 b을 계산하기 전에 a를 먼저 계산(순차적)하라는 뜻으로 이해하시면 됩니다. 따라서 위 예제는 fib (n - 2)는 별도의 쓰레드로 계산하고 동시에 부모 쓰레드에서 fib (n - 1)을 계산해 두 결과를 합쳐서 리턴하게 됩니다.

GHC에 -threaded 옵션만 주면 자동으로 멀티 코어를 활용하는 런타임이 생성됩니다. 실행할 때는 명시적으로 +RTS -N2로 코어가 2개임을 지정해주면 됩니다.

ghc -O2 fib2.hs -o fib2.exe --make -threaded
time ./fib2.exe +RTS -N2
real    0m10.171s
user    0m0.015s
sys     0m0.031s


위 프로그램은 명시적으로 쓰레드를 생성하지 않았고 락을 잡지도 않았지만 멀티 코어를 활용하게 됩니다. 수행 결과 싱글 코어일 때보다 조금 더 빠른 10.17초가 걸렸습니다. 프로그램이 작다보니 쓰레드를 생성하는데 오버헤드가 있어서 성능이 2배가 되지는 않는 걸로 보입니다. 대신에 작업 관리자를 보시면 이번에는 CPU를 거의 100% 쓰고 있다는 사실을 확인하실 수 있습니다.

제 컴퓨터에서는 테스트해볼 수 없었지만 위 프로그램은 코어 수가 2배 4배가 되면 그에 따라 성능이 증가하게 됩니다. 즉, 멀티 코어가 대세가 되면 코어를 제대로 활용하지 못하는 C 프로그램보다 여러 코어를 잘 활용할 수 있는 함수 언어가 더 빠른 세상도 올 수 있는 것입니다.


참고: 헤스켈 컴파일러인 GHC는 공유 메모리 멀티프로세서만 지원합니다. 클러스터나 싱글 멀티프로세서 등은 GHC에서 파생된 GPH(Glasgow Parallel Haskell) 프로젝트를 사용하시면 됩니다.