Search Results for 'Paralleism'

1 POSTS

  1. 2008.03.26 멀티 코어 프로그래밍과 헤스켈 (2)
멀티코어 시대가 오면서 병렬 프로그래밍이 성능 향상의 중요 방법으로 떠오르고 있습니다. 하지만 실제 개발 환경에서 개발자가 멀티 코어를 효율적으로 활용하기는 쉽지 않습니다.

다음은 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) 프로젝트를 사용하시면 됩니다.

신고

티스토리 툴바