Graphics Programming

[번역] A Tutorial on Parallel and Concurrent Programming in Haskell 본문

Season 1/하스켈

[번역] A Tutorial on Parallel and Concurrent Programming in Haskell

minseoklee 2016. 3. 7. 23:33

원문: http://research.microsoft.com/en-us/um/people/simonpj/papers/parallel/AFP08-notes.pdf


요약 형식으로 번역.


1. 서문

다양한 반-암시적(semi-implicit) 병렬화와 쓰레드 기반 명시적 병렬화 기법들, 소프트웨어 트랜잭션 메모리, 중첩 데이터 병렬화 등을 다룬다. 독자가 하스켈의 지연 함수형 프로그래밍에 익숙하다고 가정.



2. 동시성과 병렬화의 응용

동시성 병렬 프로그램을 작성하는 것은 순차적 프로그램을 작성하는 것보다 어렵다. 하지만 그럴 만한 가치가 있다.


수행능력. 멀티코어 프로세서로 수행능력을 향상할 수 있다.
지연시간 감추기. 싱글코어 프로세서에서도 동시성으로 느린 I/O 작업의 지연시간을 숨길 수 있다.
소프트웨어 구조화. 특정 종류의 프로그램들은 상호 통신하는 쓰레드들로 쉽게 표현할 수 있다. 가령 UI 컴포넌트를 별개의 쓰레드로 모델링할 수 있다.
현실의 동시성. 분산 및 실시간 시스템에서는 현실 사건을 모델링하고 또 그 사건에 반응해야 한다. 예: 여러 서버 요청을 병렬로 처리하기.


개별 코어의 성능 향상은 더 이상 기대하기 어렵다. 수행능력을 향상하는 유일한 방법은 프로그램이 하는 일을 여러 코어로 분담하는 것이다.

- 순차적 코드를 자동으로 병렬화하기. 아직 연구 주제.
- 작업이 병렬 처리되도록 사용자가 명시하고, 운영체제가 멀티코어에서 스케쥴링하기. 이 글에서 다룰 방법.



3. 병렬 하스켈 프로그램 컴파일하기

- GHC 6.10.1 이상 필요.
- 병렬 하스켈 프로그램을 컴파일하려면 -threaded 플래그 필요.
  - Wombat.hs 파일에 포함된 병렬 프로그램을 컴파일하려면: ghc --make -threaded Wombat.hs
  - 프로그램을 실행할 때는, 하스켈 프로그램 내의 논리적 쓰레드들을 실행할 때 실제 쓰레드를 몇 개 쓸 것인지를 지정해야 한다.

  - 실제 쓰레드 3개를 사용하여 Wombat 프로그램을 실행하려면: Wombat +RTS -N3
- 이 글에서 쓰레드는 운영체제 고유의 쓰레드가 아니라 하스켈 쓰레드를 지칭한다.



4. 반 명시적 병렬화
순수 하스켈 프로그램은 자동으로 병렬화할 여지가 있다. 부수효과가 없으니 모든 표현식을 수월히 병렬 평가할 수 있을 것 같지만, 효율적으로 스케줄링하기에는 너무 작은 아이템들이 만들어지고, 병렬화는 그 프로그램의 기본적인 데이터 의존성에 의해 제한된다.


하스켈은 사용자가 병렬화 수준을 제어하는 방법을 제공한다. Control.Parallel 모듈의 함수들을 사용한다.
par:: a -> b -> b
pseq :: a -> b -> b


par는 런타임 시스템이 a와 b를 병렬 평가하도록 만든다. 그리고 b를 반환한다.


런타임 시스템이 표현식 a의 값을 계산하기 위해 꼭 별도의 쓰레드를 만드는 것은 아니다. 대신 부모 쓰레드와는 다른 쓰레드에서 실행될 수도 있는 스파크(spark)를 만든다. 스파크 계산은 투기적(speculative) 실행의 가능성을 표현한다.


이런 프로그램을 반 명시적이라고 하는 이유는, 병렬화를 어느 수준으로 할 것인지에 대해 프로그래머가 힌트를 주기 때문이다. 그러면 시스템은 이 동시성을 구현하기 위해 쓰레드들을 암시적으로 생성한다. 사용자는 직접 쓰레드를 만든다거나 쓰레드간 통신, 동기화를 하거나 하지 않는다.


par 활용 예시 1 - 피보나치 함수

 

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

 

par 활용 예시 2 - sumEuler 함수

 

mkList :: Int -> [Int]
mkList n = [1..n-1]

 

relprime :: Int -> Int -> Bool
relprime x y = gcd x y == 1

 

euler :: Int -> Int
euler n = length (filter (relprime n) (mkList n))

 

sumEuler :: Int -> Int
sumEuler = sum . (map euler) . mkList

 

목표: fib와 sumEuler의 결과를 더하는 함수를 병렬화


sumFibEuler :: Int -> Int -> Int
sumFibEuler a b = fib a + sumEuler b

 

첫 번째 시도: par를 사용해서 fib 계산을 스파크로 만들고 부모 쓰레드는 sumEuler 계산하기

 

parSumFibEuler :: Int -> Int -> Int
parSumFibEuler a b = f `par` (f + e)

  where

    f = fib a

    e = sumEuler b

 

 계산 시간 재는 것은 System.Time 모듈을 이용


secDiff :: ClockTime -> ClockTime -> Float
secDiff (TOD secs1 psecs1) (TOD secs2 psecs2)
  = fromInteger (psecs2 - psecs1) / 1e12 + fromInteger (secs2 - secs1)


메인 프로그램은 충분히 큰 인자를 주어 sumFibEuler 함수를 호출하고 그 결과를 보고


r1 :: Int
r1 = sumFibEuler 38 5300


main :: IO ()
main = do
  t0 <- getClockTime
  pseq r1 (return ())
  t1 <- getClockTime
  putStrLn ("sum: " ++ show r1)
  putStrLn ("time: " ++ show (secDiff t0 t1) ++ " seconds")


쓰레드 하나로 실행할 경우 평가 순서




f의 평가를 위해 스파크가 생성되지만, 이 스파크를 인스턴스화할 다른 쓰레드가 없으므로 먼저 f를 계산하고 e를 계산해서 둘을 더한다. (+가 왼쪽 인자부터 평가한다고 가정하면)


운영체제의 진짜 쓰레드를 하나 사용할 때


$ ParSumFibEuler +RTS -N1
sum: 47625790
time: 9.274 seconds


두 개 사용할 때


$ ParSumFibEuler +RTS -N2
sum: 47625790
time: 9.223 seconds


코어 두 개를 써도 순차적 버전보다 전혀 나아지지 않는다. parSumFibEuler 함수에서 fib 38을 계산하는 일이 투기적 평가를 위해 스파크로 떨어져 나오는데, 이 프로그램에서는 왼쪽 인자를 오른쪽 인자보다 먼저 계산하는 +를 사용하기 때문에 스파크가 별도의 쓰레드로 만들어지지 않는다. 메인 쓰레드가 fib 38을 계산하고 나면 sumEuler 5300을 계산하고 수행능력 면에서 순차적 프로그램과 동등하다.




런타임 시스템에게 묻기 - 스파크가 몇 개 생성되었고 실제로 몇 개 평가되었고 각 쓰레드가 일을 얼마나 했는가? -s 플래그를 사용하면 표준 출력 또는 별도의 로그 파일에 기록


$ ParSumFibEuler +RTS -N2 -s
.\ParSumFibEuler +RTS -N2 -s
sum: 47625790
time: 9.223 seconds
...
  SPARKS: 1 (0 converted, 0 pruned)


  INIT time 0.00s ( 0.01s elapsed)
  MUT time 8.75s ( 8.79s elapsed)
  GC time 0.42s ( 0.43s elapsed)
  EXIT time 0.00s ( 0.00s elapsed)
  Total time 9.17s ( 9.23s elapsed)
...


스파크 하나를 생성했지만 별도 쓰레드에서 실행된 적이 없다. ThreadScope라는 쓰레드 프로파일러를 이용해 실행 흔적을 보면 한 쓰레드는 계속 바쁘지만 다른 쓰레드는 아무 일도 하지 않는다. 보라색은 쓰레드가 일하는 것을, 주황색은 가비지 컬렉션을 뜻한다.




해결책: +의 인자들 순서를 뒤집는다. 밑의 코드에서 (f + e)는 (e + f)로 바뀜


parSumFibEuler :: Int -> Int -> Int
parSumFibEuler a b = f `par` (e + f)

  where

    f = fib a

    e = sumEuler b


속도가 향상된다.


$ ParFibSumEuler2 +RTS -N1
sum: 47625790
time: 9.158 seconds


$ ParFibSumEuler2 +RTS -N2
sum: 47625790
time: 5.236 seconds


로그를 보면


$ .\ParFibSumEuler2 +RTS -N2 -s
.\ParSumFibEuler2 +RTS -N2 -s
...
  SPARKS: 1 (1 converted, 0 pruned)


  INIT time 0.00s ( 0.01s elapsed)
  MUT time 8.92s ( 4.83s elapsed)
  GC time 0.39s ( 0.41s elapsed)
  EXIT time 0.00s ( 0.00s elapsed)
  Total time 9.31s ( 5.25s elapsed)
...




하지만 +의 평가 순서에 의존하는 것은 아주 안 좋은 발상이다. 하스켈 언어는 +의 왼쪽, 오른쪽 인자의 평가 순서를 정의하지 않으며 컴파일러는 a + b를 b + a로 변환할 수 있다.


해결책: Control.Monad 모듈의 pseq. a `pseq` b는 a를 먼저 평가하고 b를 반환한다. pseq으로 메인 쓰레드가 어떤 일을 먼저 하도록 지정할 수 있다. parFibSumEuler를 다시 작성하면:


parSumFibEuler :: Int -> Int -> Int
parSumFibEuler a b = f `par` (e `pseq` (e + f))
  where
    f = fib a
    e = sumEuler b


다음 코드에서는 +의 인자들을 맞바꿨지만 pseq을 썼기 때문에 여전히 fib를 계산하기 전에 sumEuler를 처리함을 보장한다. (fib는 투기적으로 생성된 쓰레드에서 이미 계산되었을 것이다)

 

parSumFibEuler :: Int -> Int -> Int
parSumFibEuler a b = f `par` (e `pseq` (f + e))
  where
    f = fib a
    e = sumEuler b


 

 

 

4.1 Weak Head Normal Form (WHNF)

fib-Euler 프로그램의 변형 - 병렬로 처리되는 각 작업이 연산을 리스트에 매핑한다.

 

module Main
where
import System.Time
import Control.Parallel

 

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

 

mapFib :: [Int]
mapFib = map fib [37, 38, 39, 40]

 

mkList :: Int -> [Int]
mkList n = [1..n-1]

 

relprime :: Int -> Int -> Bool
relprime x y = gcd x y == 1

 

euler :: Int -> Int
euler n = length (filter (relprime n) (mkList n))

 

sumEuler :: Int -> Int
sumEuler = sum . (map euler) . mkList

 

mapEuler :: [Int]
mapEuler = map sumEuler [7600, 7600]

 

parMapFibEuler :: Int
parMapFibEuler = mapFib `par` (mapEuler `pseq` (sum mapFib + sum mapEuler))

 

main :: IO ()
main = putStrLn (show parMapFibEuler)

 

의도: 두 독립적인 함수를 병렬 계산한다.
- fib 함수를 리스트에 매핑하고 그 결과를 더한다
- sumEuler 함수를 리스트에 매핑하고 그 결과를 더한다

 

메인 프로그램은 두 결과를 더해 최종 결과를 산출한다. 하지만 1코어, 2코어로 이 프로그램을 실행하면 속도 차이가 없다.

 

satnams@MSRC-LAGAVULIN ~/papers/afp2008/whnf
$ time WHNF2 +RTS -N1
263935901

 

real 0m48.086s
user 0m0.000s
sys 0m0.015s

 

satnams@MSRC-LAGAVULIN ~/papers/afp2008/whnf
$ time WHNF2 +RTS -N2


263935901

real 0m47.631s
user 0m0.000s
sys 0m0.015s

 

문제: mapFib가 숫자로 완전히 평가된 값들의 리스트를 반환하지 않는다. 표현식은 WHNF로 환원되어 최상위 cons 셀이 헤드, 테일 원소들은 평가되지 않은 채다. 병렬 쓰레드에서 거의 아무 작업도 하지 않게 된다. 하스켈의 지연 평가 전략과 우리의 평가 시점을 제어하려는 의도가 충돌한다.

 

 

 

해결책: 어떻게든 리스트가 평가되도록 강제한다. 리스트의 각 원소를 훑어 pseq의 첫 인자로 넣어, 원소가 숫자로 평가되도록 하는 함수를 정의한다.

 

forceList :: [a] -> ()
forceList [] = ()
forceList (x:xs) = x `pseq` forceList xs

 

이제 mapFib를 WHNF가 아니라 완전한 리스트로 평가할 수 있다

 

module Main
where
import Control.Parallel

 

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

 

mapFib :: [Int]
mapFib = map fib [37, 38, 39, 40]

 

mkList :: Int -> [Int]
mkList n = [1..n-1]

 

relprime :: Int -> Int -> Bool
relprime x y = gcd x y == 1

 

euler :: Int -> Int
euler n = length (filter (relprime n) (mkList n))

 

sumEuler :: Int -> Int
sumEuler = sum . (map euler) . mkList

 

mapEuler :: [Int]
mapEuler = map sumEuler [7600, 7600]

 

parMapFibEuler :: Int
parMapFibEuler = (forceList mapFib) `par` (forceList mapEuler `pseq` (sum mapFib + sum mapEuler))

 

forceList :: [a] -> ()
forceList [] = ()
forceList (x:xs) = x `pseq` forceList xs

 

main :: IO ()
main = putStrLn (show parMapFibEuler)

 

mapFib와 mapEuler가 병렬 수행됨

 

satnams@MSRC-LAGAVULIN ~/papers/afp2008/whnf
$ time WHNF3 +RTS -N1
263935901

 

real 0m47.680s
user 0m0.015s
sys 0m0.000s

 

satnams@MSRC-LAGAVULIN ~/papers/afp2008/whnf
$ time WHNF3 +RTS -N2
263935901

 

real 0m28.143s
user 0m0.000s
sys 0m0.000s

 

질문: mapEuler에서 forceList 호출을 없애면 수행능력에 끼치는 영향은? pseq은 첫 인자를 WHNF로 평가한다. 표현식을 완전히 평가하지 않는다. 헤드 표현식과 테일 표현식으로 구성된 리스트를 구축하는 표현식(CONS 표현식)에서 pseq은 헤드 표현식과 테일 표현식을 평가하지 않는다. 하스켈은 seq라는 함수도 정의하지만 컴파일러는 seq의 인자들을 자유롭게 바꿀 수 있다. 평가 순서를 사용자가 제어할 수 없다. pseq은 인자들을 절대 맞바꾸지 않는다.

 

4.2 분할 정복
연습문제1: 병렬 퀵소트. 다음은 퀵정렬의 순차적 구현이다. 메인 함수는 의사 랜덤 리스트를 생성하고 입력 리스트 구축, 정렬 수행, 모든 숫자를 더하는 데 걸리는 시간을 잰다.

 

module Main
where
import System.Time
import Control.Parallel
import System.Random

 

−− A sequential quicksort
quicksort :: Ord a => [a] −> [a]
quicksort [] = []
quicksort (x:xs) = losort ++ x : hisort where
  losort = quicksort [y | y <− xs, y < x]
  hisort = quicksort [y | y <− xs, y >= x]

 

secDiff :: ClockTime −> ClockTime −> Float
secDiff (TOD secs1 psecs1) (TOD secs2 psecs2)
  = fromInteger (psecs2 − psecs1) / 1e12 + fromInteger (secs2 − secs1)

 

main :: IO ()
main = do
  t0 <− getClockTime
  let input = (take 20000 (randomRs (0,100) (mkStdGen 42)))::[Int]
  seq (forceList input) (return ())
  t1 <− getClockTime
  let r = sum (quicksortF input)
  seq r (return ()) −− Force evaluation of sum
  t2 <− getClockTime
  −− Write out the sum of the result.
  putStrLn (’’Sum of sort: ’’ ++ show r)
  −− Write out the time taken to perform the sort.
  putStrLn (’’Time to sort: ’’ ++ show (secDiff t1 t2))

 

가비지 컬렉션 횟수를 줄여 병렬 하스켈 프로그램의 성능을 높일 수 있다. 프로그램의 힙 크기를 늘리면 된다. 힙 크기는 실행 인자로서 가령 -K100M은 100MB 스택을, -H800M은 800MB 힙을 뜻한다.

 

satnams@msrc-bensley /cygdrive/l/papers/afp2008/quicksort
$ QuicksortD +RTS -N1 -H800M
Sum of sort: 50042651196
Time to sort: 4.593779

 

satnams@msrc-bensley /cygdrive/l/papers/afp2008/quicksort
$ QuicksortD +RTS -N2 -K100M -H800M
Sum of sort: 50042651196
Time to sort: 2.781196

 

GHC의 병렬 가비지 컬렉션을 활성화하고 더 나은 캐시 동작을 위해 load balancing을 비활성화할 수 있다.

 

$ ./QuicksortD.exe +RTS -N2 -K100M -H300M -qg0 -qb -s

 

 

5. 명시적 동시성

반 암시적 병렬 프로그램을 작성하여 순수 함수형 프로그램을 병렬화할 수 있다. 하지만 IO 모나드 안에서 상태 있는 계산을 병렬화할 때는 잘 먹히지 않는다. 명시적으로 쓰레드를 쓰는 프로그램을 작성해야 한다. 하스켈에서는 동시성을 위한 문법이 따로 없고 라이브러리 함수를 통해서 명시적 동시성을 제공한다.

 

5.1 하스켈 쓰레드 생성하기
기본 함수들은 Control.Concurrent 모듈에 있다. 하스켈 쓰레드의 아이디는 ThreadId 타입으로 나타낸다. IO 모나드 내에서 어떤 계산을 위해 새로운 쓰레드를 만들려면 forkIO 함수를 호출한다. 이 함수는 IO 유닛을 반환한다.

 

forkIO :: IO () -> IO ThreadId

 

forkIO가 순수 함수형 표현식 대신 IO 모나드 내의 표현식을 취하는 이유: 대부분의 동시성 프로그램은 서로 통신해야 하고 그러려면 공유 동기화 상태를 이용해야 한다. 상태 있는 작업들은 IO 모나드를 통해야 한다.

 

forkIO로 생성한 쓰레드의 주의점: 메인 프로그램(부모 쓰레드)는 자식 쓰레드가 종료될 때까지 자동으로 대기하지 않는다.

 

진짜 OS 쓰레드가 필요하면 forkOS 함수를 사용:

 

forkOS :: IO () -> IO ThreadId

 

이렇게 생성된 쓰레드는 특정한 운영체제 쓰레드에 바인딩된다. 하스켈 프로그램이 특정 종류의 외부 호출을 지원하기 위해선 이 기능이 필요하다.

 

5.2 MVar
원활한 쓰레드간 통신과 동기화를 위해 하스켈은 MVar(mutable variable)를 제공한다. MVar와 관련 명령들은 Control.Concurrent.MVar 모듈에 있다. MVar는 비어있거나 하나의 값을 포함한다. 점유된 MVar에 값을 쓰려는 쓰레드는 중단되고 MVar가 빌 때 깨어난다. 해당 쓰레드는 이제 값 쓰기를 다시 시도할 수 있다. 여러 쓰레드가 MVar에 값 쓰기를 위해 대기하고 있다면 시스템은 FIFO 전략을 사용하여 가장 오래 기다린 쓰레드를 깨운다. 빈 MVar로부터 값을 읽으려는 쓰레드는 중단되고 그 MVar에 값이 쓰일 때 깨어난다. 여러 쓰레드가 읽기를 기다리고 있다면, 가장 오래 기다린 쓰레드가 깨어난다.


빈 MVar 생성, 초기값을 가진 MVar 생성, MVar로부터 값 제거, MVar 내의 값 관찰 등을 위한 명령들이 제공된다.

 

data MVar a

 

newEmptyMVar :: IO (MVar a)
newMVar :: a −> IO (MVar a)
takeMVar :: MVar a −> IO a
putMVar :: MVar a −> a −> IO ()
readMVar :: MVar a −> IO a
tryTakeMVar :: MVar a −> IO (Maybe a)
tryPutMVar :: MVar a −> a −> IO Bool
isEmptyMVar :: MVar a −> IO Bool
−− 기타 등등

 

MVar 한 쌍과 블로킹 명령인 putMVar, takeMVar를 활용해 두 쓰레드간 접선 약속(rendezvous)을 구현할 수 있다.

 

module Main
where
import Control.Concurrent
import Control.Concurrent.MVar

 

threadA :: MVar Int −> MVar Float −> IO ()
threadA valueToSendMVar valueReceiveMVar = do
  −− some work
  −− now perform rendezvous by sending 72
  putMVar valueToSendMVar 72 −− send value
  v <− takeMVar valueReceiveMVar
  putStrLn (show v)

 

threadB :: MVar Int −> MVar Float −> IO ()
threadB valueToReceiveMVar valueToSendMVar = do
  −− some work
  −− now perform rendezvous by waiting on value
  z <− takeMVar valueToReceiveMVar
  putMVar valueToSendMVar (1.2 * z)
  −− continue with other work

 

main :: IO ()
main = do
  aMVar <− newEmptyMVar
  bMVar <− newEmptyMVar
  forkIO (threadA aMVar bMVar)
  forkIO (threadB aMVar bMVar)
  threadDelay 1000 −− threadA와 threadB가 종료되길 기다린다 (대충)

 

연습문제2: 위 프로그램을 고쳐 threadDelay를 없애고 메인 쓰레드가 모든 자식 쓰레드가 완료될 때까지 기다리는 보다 확실한 방법을 사용할 것.

 

module Main
where
import Control.Parallel
import Control.Concurrent
import Control.Concurrent.MVar

 

fib :: Int −> Int
−− As before

 

fibThread :: Int −> MVar Int −> IO ()
fibThread n resultMVar = putMVar resultMVar (fib n)

 

sumEuler :: Int −> Int
−− As before

 

s1 :: Int
s1 = sumEuler 7450

 

main :: IO ()
main = do
  putStrLn ”explicit SumFibEuler”
  fibResult <− newEmptyMVar
  forkIO (fibThread 40 fibResult)
  pseq s1 (return ())
  f <− takeMVar fibResult
  putStrLn (”sum: ” ++ show (s1+f))

 

쓰레드 한 개, 두 개로 실행한 결과:

 

satnams@MSRC-1607220 ~/papers/afp2008/explicit
$ time ExplicitWrong +RTS -N1
explicit SumFibEuler


sum: 119201850

real 0m40.473s
user 0m0.000s
sys 0m0.031s

 

satnams@MSRC-1607220 ~/papers/afp2008/explicit
$ time ExplicitWrong +RTS -N2
explicit SumFibEuler


sum: 119201850

real 0m38.580s
user 0m0.000s
sys 0m0.015s

 

fib 계산이 fibThread 쓰레드 내에서 완전히 수행되어야 한다. pseq 이용.

 

module Main
where
import Control.Parallel
import Control.Concurrent
import Control.Concurrent.MVar

 

fib :: Int −> Int
−− As before

 

fibThread n resultMVar = do
  pseq f (return ())
  putMVar resultMVar f
  where f = fib n

 

sumEuler :: Int −> Int
−− As before

 

s1 :: Int
s1 = sumEuler 7450

 

main :: IO ()
main = do
  putStrLn ”explicit SumFibEuler”
  fibResult <− newEmptyMVar
  forkIO (fibThread 40 fibResult)
  pseq s1 (return ())
  f <− takeMVar fibResult
  putStrLn (”sum: ” ++ show (s1+f))

 

MVar를 써서 프로그램을 작성하다 보면 데드락에 걸리기 쉽다. 어떤 쓰레드가 MVar에 값이 쓰이길 기다리고 있지만 다른 쓰레드들은 MVar에 값을 영영 쓰지 않는다. 하스켈은 명시적 락 대신 소프트웨어 트랜잭션 메모리(STM)를 제공한다. (Control.Concurrent.STM 모듈)

 

data STM a −− A monad supporting atomic memory transactions
atomically :: STM a −> IO a −− Perform a series of STM actions atomically
retry :: STM a −− Retry current transaction from the beginning
orElse :: STM a −> STM a −> STM a −− Compose two transactions
data TVar a −− Shared memory locations that support atomic memory operations
newTVar :: a −> STM (TVar a) −− Create a new TVar with an initial value
readTVar :: TVar a −> STM a −− Return the current value stored in a TVar
writeTVar :: TVar a −> a −> STM () −− Write the supplied value into a TVar

 

TVar는 아토믹 블록(atomic block) 안에서만 사용 가능한 타입. 아토믹 블록 안에서는 TVar에 읽고 쓰기 가능. 밖에서는 TVar를 생성하고 인자로 넘기는 것만 가능. 특별한 글로벌 락이 하나 있어서, 모든 아토믹 블록은 이 락을 취하고 코드를 실행한 다음 락을 푸는 것으로 이해. 아토믹 블록 안의 코드는 다른 쓰레드와 병렬 실행되거나 간섭받지 않을 것처럼 보이지만 사실은 그렇게 된다. 대신 블록의 실행을 되돌릴 로그를 활용.

 

atomically 함수로 아토믹 블록을 실행.

 

예: STM으로 두 쓰레드에서 공유 변수 수정하기. 이것은 STM의 모델일 뿐이며 실제 구현은 훨씬 복잡함.

 

쓰레드 1은 공유 TVar를 원자적으로 1 증가시킨다.

 

atomically (do
  v <− readTVar bal
  writeTVar bal (v+1))

 

쓰레드 2는 공유 TVar를 원자적으로 3 감소시킨다.

 

atomically (do
  v <− readTVar bal
  writeTVar bal (v-3))

 

쓰레드 1과 2는 아토믹 블록에 들어가며 각각 bal에 대한 트랜잭션 로그를 생성.

쓰레드 1은 1을 증가시킨 값을, 쓰레드 2는 3을 뺀 값을 자신의 로그에 저장.

커밋할 때 원래 bal 값과 로그에 처음 저장된 bal 값이 같은지 검사. 같으므로 쓰레드 1은 bal의 값을 변경하고 로그 파기. 쓰레드 2는 두 값이 맞지 않으므로 자신의 아토믹 블록을 재실행.

 

retry 함수는 현재 트랜잭션을 중단하고 새로운 로그를 가지고 처음부터 재실행. 모듈러 블로킹(modular blocking) 구현 가능. 트랜잭션이 성공적으로 커밋되지 않음을 알 때 유용. 다음은 계좌에서 돈을 빼는 트랜잭션. 돈이 충분하지 않으면 트랜잭션을 재시도한다.

 

withdraw :: TVar Int -> Int -> STM ()
withdraw acc n = do
  bal <- readTVar
  if bal < n then retry
  writeTVar acc (bal-n)

 

orElse 함수는 두 트랜잭션을 결합해 선택의 기회를 제공. 한 트랜잭션이 실패하면 다른 트랜잭션을 실행. 둘 다 실패하면 전체 트랜잭션을 재실행. 다음은 계좌 a1에서 인출을 시도하고 실패하면(즉 retry가 호출되면) 계좌 a2에서 인출을 시도. 그리고 계좌 b에 입금. 실패하면 전체 트랜잭션 재실행.

 

atomically (do { withdraw a1 3
                 `orElse`
                 withdraw a2 3;
                 deposit b 3 }
           )

 

하스켈 쓰레드간 공유되는 큐 만들기. 큐는 고정 크기 배열로 표현. 꽉 찬 큐에 기록하거나 빈 큐를 읽으려는 쓰레드는 중지된다.

 

STM 기반 큐의 데이터타입 선언.

 

data Queue e
  = Queue
    { shead :: TVar Int,
      stail :: TVar Int,
      empty :: TVar Bool,
      sa :: Array Int (TVar e)
    }

 

빈 큐의 묘사.

 

 

연습문제. 다음 연산들을 구현할 것.

 

- 새로운 비어있는 큐 생성.
newQueue :: IO (Queue a)

 

- 큐에 원소 추가.
enqueue :: Queue a -> a -> IO ()
큐가 꽉 찼으면 공간이 날 때까지 중단.

 

- 큐에서 원소 제거.
dequeue :: Queue a -> IO a
큐가 비었으면 제거할 원소가 들어올 때까지 중단.

 

- 큐에서 값 읽기. 비었으면 다른 큐에서 시도. 둘 다 비었으면 하나가 이용 가능할 때까지 중단.
dequeueEither :: Queue a -> Queue a -> IO a

 

 

6. 중첩 데이터 병렬화

지금까지 본 병렬화를 하는 두 방법
- par/seq: 문법적으로 투명. 새 쓰레드를 생성할 만큼 granularity가 일관되게 큰지 확신하기 어려움.
- 명시적으로 쓰레드를 fork해서 MVar 또는 STM 사용: 프로그래머가 granularity를 정밀 제어. 문법적으로 복잡함. 많은 쓰레드가 공유 변수를 수정하게 된다. 쓰레드간 조율이 어렵다.

 

둘 다 분산 메모리 기계에서 구현하기 어렵다. 어떤 포인터도 임의 값을 가리킬 수 있어 공간 국지성이 형편없다. 이런 무질서한 메모리 모델을 분산 메모리 아키텍처에서 지원하는 것이 가능하긴 하지만 믿을 만하고 예측 가능하고 규모 가변적인 수행능력은 요원하다. 즉 좋은 수행능력 모델이 없다. 병렬 프로그램을 작성하는 목적이 수행능력을 향상시키는 것인데도.

 

또다른 병렬 프로그래밍 패러다임: 데이터 병렬성.

 

기본 발상: 거대한 값의 집합의 모든 원소에 대해, 같은 일을 병렬로 하라.

 

- 모든 것이 par/seq처럼 순수 함수형이다. 새로운 문법 불필요.
- granularity가 매우 좋다. 물리적 프로세서마다 한 쓰레드만 있다.
- 국지성이 매우 좋다. 데이터는 힙 사이를 가로지르는 포인터 없이, 프로세스별로 물리적으로 나뉜다.

 

6.1 플랫 데이터 병렬화

 

데이터 병렬화는 널리 성공적했지만, 데이터 병렬화에 적합한 응용 분야는 적다. 주류 데이터 병렬 기술이 플랫 데이터 병렬화만 지원하기 때문.

 

플랫 데이터 병렬화: 하나의 순차적 함수 f를, 값들의 거대한 집합 a의 모든 원소에, 병렬로 적용한다. f가 순차적일 뿐만 아니라, 집합의 모든 원소에 대해 수행시간이 비슷하다.

 

데이터 병렬 하스켈에서 위와 같은 루프를 작성하는 방법:

 

sumSq :: [: Float :] -> Float
sumSq a = sumP [: x*x | x <- a :]

 

데이터 타입 [: Float :]는 Float의 병렬 벡터(parallel vector of Float)이라고 발음. 각괄호 표기를 쓰는 것은 병렬 벡터가 리스트와 닮았기 때문. 많은 리스트 관련 함수가 병렬 벡터에서도 비슷한 것이 있다.

 

mapP :: (a -> b) -> [:a:] -> [:b:]
zipWithP :: (a -> b -> c) -> [:a:] -> [:b:] -> [:c:]
sumP :: Num a => [:a:] -> a

 

(+:+) :: [:a:] -> [:a:] -> [:a:]
filterP :: (a -> Bool) -> [:a:] -> [:a:]
anyP :: (a -> Bool) -> [:a:] -> Bool
concatP :: [:[:a:]:] -> [:a:]
nullP :: [:a:] -> Bool
lengthP :: [:a:] -> Int
(!:) :: [:a:] -> Int -> a -- 인덱스 0부터 시작

 

위 함수들은 Data.Array.Parallel 모듈에서 제공. 리스트 조건제시식이 있듯이 병렬 배열 조건제시식도 있다. 문법적 편의일 뿐이며 다음과 같이 쓸 수도 있었다.

 

sumSq :: [: Float :] -> Float
sumSq a = sumP (mapP (\x -> x*x) a)

 

forkIO도 par도 없다. 병렬성은 병렬 벡터에 대한 원시 연산들, mapP, sumP 등에서 온다.

 

플랫 데이터 병렬화는 단일 배열에 제한되지 않는다. 두 벡터의 내적을 취하는 방법:

 

vecMul :: [:Float:] -> [:Float:] -> Float
vecMul a b = sumP [: x*y | x <- a | y <- b :]

 

두 번째 "|" 막대는 a와 b를 동시에 훑는 것을 뜻한다(리스트 조건제시식에서도 가능하다). 역시 문법적 편의로 다음과 동등.

 

vecMul :: [:Float:] -> [:Float:] -> Float
vecMul a b = sumP (zipWithP (*) a b)

 

6.2 플랫 데이터 병렬화의 장단점
플랫 데이터 병렬화를 이용해 프로그램을 표현하면 N-프로세서 장치에서 훌륭하게 구현 가능하다.

 

- a를 N개 덩어리로 나누어 프로세스마다 할당한다.
- f를 각 원소에 적용하는 순차 루프를 컴파일한다.
- 이 루프를 프로세스마다 실행한다.
- 결과를 종합한다.

 

granularity: 훌륭. 국지성: 훌륭. 부하 균형: 훌륭.

 

프로그래밍 모델: 끔찍. 모든 병렬화는 단일 병렬 루프에서. mapP를 이용한 함수 g는 다른 데이터 병렬 매핑에서 호출 불가. (예: mapP g a) mapP의 인자는 순차적 함수여야 하기 때문.

 

제어 구조가 평면적이어야 하기 때문에 자료 구조도 그래야 한다. a는 풍부한 중첩 구조를 가질 수 없다. (예: a의 원소들 자체는 벡터일 수 없다)

 

6.3 중첩 데이터 병렬화
90년대 초기 Guy Blelloch가 중첩 데이터 병렬 프로그래밍을 언급. 발상:

 

하나의 함수 f를, 값들의 거대한 집합 a의 모든 원소에, 병렬 적용. 하지만 f 자체가 (중첩된) 데이터 병렬 함수일 수 있고, a의 각 원소에 비슷한 수행시간이 걸릴 필요가 없다.

 

행렬과 벡터의 곱셈:

 

type Vector = [:Float:]
type Matrix = [:Vector:]

 

matMul :: Matrix -> Vector -> Vector
matMul m v = [: vecMul r v | r <- m :]

 

vecMul을 이용해 행렬의 각 행을 벡터와 곱셈. 데이터 병렬 함수 vecMul을 데이터 병렬 연산(matMul의 조건제시식) 내에서 호출.

 

이런 간단한 경우는 컴파일러 기술의 발달로 인해, 중첩 루프를 하나의 루프로 펼치는 것이 가능. 하지만 행렬이 성긴(sparse) 행렬이라면?

 

성긴 벡터(또는 행렬)는 거의 모든 원소가 0이다. 쌍의 벡터로 표현 가능.

 

type SparseVector = [: (Int, Float) :]

 

0이 아닌 원소들만 인덱스와 값으로 표현. 성긴 행렬은 각각이 성긴 벡터인 행의 벡터로 표현.

 

type SparseMatrix = [: SparseVector :]

 

vecMul과 matMul은 이렇게 변경:

 

sparseVecMul :: SparseVector -> Vector -> Float
sparseVecMul sv v = sumP [: x * v!:i | (i,x) <- sv :]


sparseMatMul :: SparseMatrix -> Vector -> Vector
sparseMatMul sm v = [: sparseVecMul r v | r <- sm :]

 

밀집 벡터 v의 색인을 위해 (!:) 연산자를 사용. 제어 구조는 전과 같지만(중첩 루프, 두 수준 다 데이터 병렬) 자료 구조는 덜 정규화, 프로그램을 어떻게 단일 데이터 병렬 루프로 펼칠지 애매함. 행렬의 0이 아닌 데이터들의 분포에 무관하게 N개 프로세서에 일 배분하는 방법은?

 

Blelloch: 중첩 데이터 병렬을 쓰는 어떤 프로그램이든(작성하긴 쉽지만 효율적 구현은 어려움), 플랫 데이터 병렬을 쓰는 프로그램으로(작성하기 어렵지만 효율적 구현 쉬움) 변환 가능함을 보임. 특수 목적 함수형 언어 NESL으로 이를 설명함.

 

하지만 NESL은 실용적이지 않다. 일급 언어로서 한정된 데이터 타입만 있고 인터프리터로 구현됨. Manuel Chakravarty, Gabriele Keller, ROman Leshchinskiy등이 Blelloch의 변환법을 일반화해, 사용자 정의 대수적 데이터 타입이 있는 현대적인 고차 함수형 언어 즉 하스켈에서 적용 가능하도록 만듦. GHC에 있는 Data Parallel Haskell은 이 아이디어의 프로토타입.

 

데이터 병렬 하스켈로 무엇이 가능한가? 다음 예제들은 Darcs 저장소(http://darcs.haskell.org/packages/ndp)의 하위 디렉토리 examples/ 에 있다. NESL로 작성된 데이터 병렬 알고리즘들은 http://www.cs.cmu.edu/~scandal/nesl/algorithms.html 에서 찾을 수 있다.

 

6.4 단어 검색

소형 웹 검색 엔진. Document는 단어들의 벡터. 각 단어는 문자열. 문서들의 거대한 집합에서 한 단어의 모든 출현을 찾아서, 해당 문서의 해당 위치를 반환하기. search 함수의 타입 시그너처:

 

type Document = [: String :]
type DocColl = [: Document :]


search :: DocColl -> String -> [: (Document, [:Int:]) :]

 

쉬운 문제부터 시작. 한 문서에서 한 단어의 모든 출현 찾기.

 

wordOccs :: Document -> String -> [:Int:]
wordOccs d s = [: i | (i,s2) <- zipP [:1..lengthP d:] d, s == s2 :]

 

배열 조건제시식에서 필터를 사용하여, s==s2인 (i,s2) 쌍만 선택. 이 필터링은 데이터 병렬적으로 수행.

 

search 함수 작성

 

search :: [: Document :] -> String -> [: (Document, [:Int:]) :]
search ds s = [: (d,is) | d <- ds
                                 , let is = wordOccs d s
                                 , not (nullP is) :]

 

6.5 소수
특정 숫자 n까지의 모든 소수를 에라토스테네스의 체로 찾는 문제. 지연 평가를 이용한 해법:

 

primes :: [Int]
primes = 2 : [x | x <- [3..], not (any (‘divides‘ x) (smallers x))]
  where smallers x = takeWhile (\p -> p*p <= x) primes
divides :: Int -> Int -> Bool
divides a b = b ‘mod‘ a == 0

 

소수 후보 x에 대해 x의 제곱근보다 낮은 소수들로 나눠지는지 검사. 병렬로 만드는 방법은?

 

primesUpTo :: Int -> [: Int :]
primesUpTo 1 = [: :]
primesUpTo 2 = [: 2 :]
primesUpTo n = smallers +:+
               [: x | x <- [: ns+1..n :]
                    , not (anyP (‘divides‘ x) smallers) :]
  where
    ns = intSqrt n
    smallers = primesUpTo ns

 

조건을 계산하는 것 자체가 중첩 데이터 병렬 계산. smallers를 계산하기 위해 primesUpTo 재귀 호출. 이전 예제들과 달리 데이터 병렬 중첩의 깊이가 동적으로 결정된다. 하지만 충분히 효율적으로 처리 가능.

 

6.6 퀵소트
지금까지는 각 데이터 병렬 연산이 거대한 집합에 대해 적용되었다. 집합이 훨씬 작다면? 분할 정복 알고리즘은 문제를 작은 하위 문제들로 나누어 각각을 풀고 그 결과를 종합. 중첩 데이터 병렬에 적합한가? 그렇다.

 

qsort :: [: Double :] -> [: Double :]
qsort xs | lengthP xs <= 1 = xs
         | otherwise = rs!:0 +:+ eq +:+ rs!:1
         where
           p = xs !: (lengthP xs ‘div‘ 2)
           lt = [:x | x <- xs, x < p :]
           eq = [:x | x <- xs, x == p:]
           gr = [:x | x <- xs, x > p :]
           rs = mapP qsort [: lt, gr :]

 

2원소 배열 [: lt, gr :]에 대해 mapP를 사용한 것이 중요. 데이터 병렬적으로 qsort를 lt와 gr에 적용. 벡터에 원소가 두 개만 있어도 상관 없음. 퀵소트가 생성하는 정렬 작업의 이진 트리를 시각화하면  각 수직 층이 데이터 병렬 수행됨.

 

6.7 Barnes Hut
지금까지는 간단한 플랫 또는 중첩 집합에 대해 작업. 병렬 배열의 각 원소가 재귀적이고 사용자 정의 대수적 자료형인 경우는?

 

Barnes-Hut n체 알고리즘의 구현. 크게 두 단계로 나누면 하나, 데이터를 계층적 트리 구조에 모은다. 둘, 트리 내의 데이터를 순회한다. 트리에서 같은 수준에 있는 데이터에 병렬로 계산을 적용해야 한다.

 

n체 알고리즘은 입자들의 모음에서 각 쌍이 작용하는 힘을 계산하여 상호작용을 결정. 정확한 해법은 n^2개 힘의 계산이 필요. Barnes-Hut 알고리즘은 입자들을 위치에 따라 셀로 묶어 계층화하여 계산 수를 최소화. 입자의 가속도를 해당 셀의 중심점을 이용하여 근사 계산. 입자는 MassPoint 타입으로 표현.

 

type Vec = (Double, Double)
type Area = (Vec, Vec)
type Mass = Double
type MassPoint = (Vec, Mass)

 

트리는 중심점과 하위 트리들의 병렬 배열을 포함하는 노드

 

data Tree = Node MassPoint [:Tree:]

 

bhTree의 각 반복에서 현재 입자 집합과 그 입자들이 있는 영역을 인자로 받는다. 영역을 동일 크기의 4개 하위 영역 subAs으로 나눈다. 각 하위 영역에 있는 입자들을 묶는다. 이 하위 집합에 대해 bhTree를 재귀 호출한다. 한 영역에 한 입자만 있을 때 재귀가 멈춘다.

 

 

 

6.8 수행능력 모델
bhTree p area = Node p [::]
bhTree ps area =
  let
    subAs = splitArea area
    pgs = splitParticles ps subAs
    subts = [: bhTree pg a| pg <- pgs | a <- subAs :]
    cd = centroid [:mp | Node mp _ <- subts :]
  in Node cd subts

 

bhTree가 계산한 트리를 이용해 accels 함수에서 각 입자에 작용하는 힘을 계산. 입자들의 집합을 두 하위집합으로 나눈다. fMps은 충분히 먼 입자들을 포함, cMps는 트리 루트에 저장된 중심점에 가까운 입자들을 포함. fMps에 있는 입자들에 의한 가속은 입자와 중심점 사이의 상호작용으로 근사 계산. cMps를 가지고 각 하위트리에 대해 accels 재귀 호출. 집합에 파티클 안 남을 때까지 반복.

 

accels:: Tree -> [:MassPoint:] -> [:Vec:]
accels _ [::] = [::]
accels (Node cd subts) mps =
  let
    (fMps, cMps) = splitMps mps
    fAcs = [:accel cd mp | mp <- fMps:]
    cAcs = [:accels t cMps| t <- subts:]
  in combine farAcs closeAcs

 

accel :: MassPoint -> MassPoint -> Vec
-- Given two particles, the function accel computes the
-- acceleration that one particle exerts on the other

 

트리는 레벨 별로 구축, 순회된다. 한 병렬 단계마다 한 레벨의 모든 노드 처리. 이는 데이터 국소성과 부하 균형을 위해 컴파일러에게 중요한 정보.

 

6.8 수행능력 모델
데이터 병렬 프로그래밍의 이점: 병렬 장치에서 프로그램 동작을 합리적으로 예측 가능한 수행능력 모델. 규모가변성 - 프로세서를 늘리면 수행능력이 어떻게 변하는지도 예측 가능.

 

수행능력 추론을 위한 두 개념: work와 depth
- work: 단일 프로세서에서 실행할 때 걸릴 시간 W.
- depth: 무한히 많은 프로세서에서 실행할 때 걸릴 시간 D. mapP 또는 다른 데이터 병렬 원시형이 실행될 때 추가 프로세서를 투입한다고 가정.

프로그램의 데이터 흐름 다이어그램을 그리면 work는 다이어그램 내 노드의 개수. depth는 입력에서 출력으로 가는 최장 거리.

 

프로세서가 무한할 수는 없으므로 P개 프로세서를 가정. 작업을 완벽히 나누면 실행 시간 T는 W/P. depth D가 매우 크면 그렇지 않음.

 

W/P <= T <= W/P + L*D

 

L은 통신 지연 상수. 이는 대역폭 제한을 고려하지 않은 근사.

 

work를 계산할 땐 지연 평가가 아니라 철저한 평가를 가정. 모든 하위 표현식을 평가하기 때문.

 

depth를 계산할 때 데이터 병렬을 고려. 다음은 닫힌 표현식 e의 depth를 계산하는 식. D[e]는 "e의 depth"를 의미.

 

다음 아이디어들을 이용.
- 기본적으로 실행은 순차적. 덧셈의 depth는 그 인자들의 depth의 합.
- 병렬 원시형 mapP 그리고 이와 관련된 filterP 등은 병렬화 가능. 따라서 depth는 최악의 원소의 depth와 동일.
- 병렬 환원 원시형 sumP 등은 배열 길이의 로그에 비례하는 시간이 걸림.

 

 

 

 

 

 

 

지연 시간 상수 L의 값은? 점근적 규모가변성(Asymptotic Scalability) 성질:

 

문제의 크기가 증가할 때 D가 W보다 점근적으로 느리게 커진다면, 프로그램은 점근적 규모가변성을 가진다.

 

문제가 충분히 크고 네트워크 대역폭이 충분하면 수행능력은 프로세서 개수에 선형적으로 변한다.
- sumSq와 search 함수는 AS 성질을 만족.
- 퀵소트의 경우 깊이가 배열 길이의 로그에 비례. 피봇이 적절히 선택된다고 가정하면 W = O(nlogn)이고 D = O(logn)이므로 AS 성질을 가진다.
- 소수 계산에서 깊이는 더 작다. D = O(loglogn). 매 단계마다 n의 제곱근을 취하기 때문에 depth d에서 n = 2^(2^d). 거의 모든 작업은 최상위 레벨에서 처리. 각 레벨에서 하는 일은 sqrt(n)과 n 사이의 수들을 sqrt(n)보다 작은 소수들과 비교하는 것. sqrt(n)보다 작은 소수는 대략 sqrt(n)/logn개. 총 작업량은 W = O(n^(3/2) / logn). 역시 AS 성질 만족.

 

6.9 작동 원리
NESL의 핵심: 중첩 데이터 병렬을 플랫 데이터 병렬로 변환할 수 있다. 중첩에서 플랫으로 변환하는 것을 벡터화 변환이라 부른다.
- 데이터를 변환하여 모든 병렬 배열이 원시형(Int, Float, Double 등)만을 포함하도록 만든다.
- 코드를 변환하여 이 플랫 데이터를 조작하도록 만든다.

 

데이터 변환하기
Float의 병렬 배열은 IEEE 부동소수점의 연속된 배열로 표현된다. Int와 Double도 비슷. 병렬 배열 타입을 이렇게 정의되었다고 볼 수 있다.

  data instance [: Int :] = PI Int ByteArray
  data instance [: Float :] = PF Int ByteArray
  data instance [: Double :] = PD Int ByteArray

 

쌍의 병렬 배열은? 힙에 할당된 쌍에 대한 포인터들의 벡터로 표현하면 주소 공간 상에 흩어지기 때문에 안된다. 배열의 쌍으로 나타내면 국지성 해결.

 

data instance [: (a,b) :] = PP [:a:] [:b:]

 

병렬 배열의 병렬 배열은? 하위 벡터들을 하나의 거대한 벡터로 통합. 각 하위 벡터가 시작하는 위치를 나타내는 첨자들의 벡터를 추가.

 

data instance [: [:a:] :] = PA [:Int:] [:a:]

 

성긴 행렬의 데이터 타입:

 

type SparseMatrix = [: SparseVector :]
type SparseVector = [: (Int, Float) :]

 

예제 행렬:

 

m :: SparseMatrix
m = [: [:(1,2.0), (7,1.9):], [:(3,3.0):] :]

 

다음과 같이 표현:

 

PA [:0,2:] (PP [:1, 7, 3 :] [:1.0, 1.9, 3.0:])

 

배열 자체는 바이트 배열로 표현:

 

PA (PI 2 #<0x0,0x2>)
     (PP (PI 3 #<0x1, 0x7, 0x3>)
           (PF 3 #<0x9383, 0x92818, 0x91813>))

 

코드 벡터화
sumSq a = sumP [: x*x | x <- a :]

 

a가 거대한 배열이면 또다른 거대한 배열을 새로 계산하는 것은 어리석다. 각 프로세서가 할당된 덩어리의 합을 계산하여 총합을 계산하면 훨씬 효율적.

 

중간 배열을 제거하는 것을 fusion이라 부른다. 데이터 병렬 하스켈의 상수 인자를 개선하는 핵심. 벡터화가 많은 중간 배열을 도입하기 때문에 fusion이 더욱 중요. 상수 인자는 실전에서 매우 중요.

 

6.10 데이터 병렬 하스켈 실행하기
- GHC 6.6.x 또는 6.8.x 버전 사용
  * 현재 최신 버전은 GHC 7.10.3
- 플래그 -fparr와 -XParallelListComp 사용
- GHC.PArr 모듈 들여오기

 

6.11 더 읽을거리

원문 참조

Comments