병렬화를 통한 연산 성능 향상


성능 측정 방법: system.time과 R profiler

특정함수에 대한 시간만 보고싶으면 간단하게 system.time함수를 이용한다.

아래와 같이 do.call과 ldply의 성능을 비교한 것이다.
usersyselapsed이 세가지 관점으로 시간을 나타내주게 된다.

> ptList <- list(1:100000)
> system.time(do.call(rbind,ptList))
 사용자  시스템 elapsed 
      0       0       0 
> system.time(ldply(ptList,rbind))
 사용자  시스템 elapsed 
   0.17    0.00    0.17 

사용자(user): 시간의 경우 프로그램 코드 자체를 R에서 수행한 시간이다.

시스템(sys): 시간의 경우 운영체제(OS)가 대신 작업을 처리하는데 걸린 시간이다. 예를 들면 save()또는 load()함수의 경우 sys 시간이 더 길게 된다. 이는 OS 작업을 통해서 파일 입출력을 수행하기 때문이다.
sys 시간이 크다면 메모리 부족도 의심해 봐야한다. R은 기본적으로 메모리에 모든 오브젝트를 로드하고 처리하므로 만약 메모리가 부족할경우 페이지 폴트가 지속적으로 발생해서 성능저하의 원인이 된다.

경과(elapsed): 시간는 가장 이해하기 쉬운 개념으로 프로그램 시작할 때 시간을 측정해서 끝날 때 종료한 시간 차이이다.

병렬 프로세싱 (doParallel, Window도 가능)

doMC는 내부적으로 fork()를 이용하기 때문에 linux에서 동작 하게 된다.
doParallel는 윈도우에서 사용 할 수 있는 package를 의미한다.

상세 관련 문서 vignette("gettingstartedParallel")

doParallel은 내부적으로 multicoreforeachsnow를 사용하기 때문에 snow의 기능인 다수의 머신에 클러스터를 구현하는 기능을 제공하므로 doMC보다 더 막강하다고 할 수 있다.

install.packages("doParallel")
library(doParallel)

프로세스의 수 설정

코어의 수를 설정 한다.

registerDoParallel(cores=4)

작업관리자를 보면, Rscript.exe가 존재 하는 것을 알 수 있다.

plyr의 병렬화

{adl}{adl_}ply

형태의 함수들의 도움말을 살펴보면 .parallel 옵션이 있다. 이 옵션 값이 TRUE로 설정 되면
registerDoParallel()을 사용해 설정한 만큼의 프로세스가 동시에 실행되어 데이터를 병렬적으로 처리한다.

foreach의 병렬화

이미 for보다는 apply 계열의 함수가 더 빠르다는 것을 알고 있다.
foreach는 이러한 apply보다 더 빠른 loop연산 함수 이다. 그 이유가 바로 간단한 병렬화를 지원하기 때문이다.

foreach()는 기본적으로 %do를 사용해 실행할 명령의 블록을 지정해야 한다.
만약 foreach()에서 블록 내 명령을 병렬로 수행하고자 한다면 registerDoParallel()을 한 뒤 foreach()에 %do% 대신 %dopar%를 지정하면 된다.

registerDoParallel로 core 수를 결정하면 실행 중인 프로세스 목록에 Rscript.exe라는 프로세스들이 설정한 코어 갯수 만큼 생성된것을 알 수 있다. 반복적으로 registerDoParallel을 실행하면 해당 프로세스가 계속해서 늘어나기 때문에 이를 막기 위해서 명시적으로stopImplicitCluster()를 해주는 것이 좋다. R-session을 종료하면 자동으로 정리해 주긴 하지만 말이다.

foreach는 apply()계열의 함수와 for()를 대체할 수 있는 루프문을 위한 함수 이다.
for문과의 가장 큰 차이는 반환 값이 있다는 점이다. 또한 {}가 아닌 %do%문을 사용해 블록을 지정한다는 점이다.

foreach::foreach(
    ... # 표현식 ex에 넘겨줄 인자
    # .combine은 ex에서의 반환 값을 어떻게 합칠지 지정한다. cbind(컬럼 방향으로 합친 데이터 프레임 반환),
    # rbind(행 방향으로 합친 데이터 프레임 반환), c(벡터로 반환) 등이 그예다. 기본값은 결과를 리스트로 합친다.
    .combine
    %do% ex # ex는 평가할 표현식
)

단순히 foreach만 사용할 경우

system.time(l1 <- rnorm(10000000))

library(foreach)
system.time(l2 <- foreach(i=1:4, .combine = 'c')
            %do% rnorm(2500000))

> system.time(l1 <- rnorm(10000000))
 사용자  시스템 elapsed 
   0.75    0.00    0.75 
> 
> library(foreach)
> system.time(l2 <- foreach(i=1:4, .combine = 'c')
+             %do% rnorm(2500000))
 사용자  시스템 elapsed 
   0.78    0.00    0.78 

오히려 속도가 느려지는 것을 알 수 있다.

doParallel과 같이 이용
좀더 차이를 주기위해서 이전 예제보다 10대 갯수를 증가 시켰다.

> system.time(l1 <- rnorm(100000000))
 사용자  시스템 elapsed 
   7.38    0.14    7.55 
> 
> library(foreach)
> system.time(l2 <- foreach(i=1:4, .combine = 'c')
+             %do% rnorm(25000000))
 사용자  시스템 elapsed 
   8.28    0.28    8.58 

> library(doParallel)
> registerDoParallel(cores = 8)
> system.time(l3p <- foreach(i=1:10, .combine = 'c')
+             %dopar% rnorm(10000000))
 사용자  시스템 elapsed 
   1.54    2.25    5.15 
> stopImplicitCluster()

> length(l1)
[1] 100000000
> length(l2)
[1] 100000000
> length(l3p)
[1] 100000000

코어를 8개로 병렬화 했지만 8배까지는 아니고 2배정도 향상된것 같다.
데이터 분할 및 병합 overhead 때문에 발생하는 통상의 문제이다.
또한 i7-processor 특징상 물리적으로는 4-cores 지만 HT기술을 고려해서 논리적으론 8개로 잡았다.

작업이 끝나면 stopImplicitCluster()를 실행해서 closing doParallel cluster를 수행 시켜 주자.

하지만, stopImplicitCluster()을 수행할경우 apply()계열의 함수들이 connection error가 발생 하므로
모든 작업이 끝난 다음에 한번만 호출해 주자.

병렬 프로세싱 (Parallel package, Linux 전용)

set up worker processes that can complete tasks simultaneously.

It does this by including components of the multicore and snow packages, each taking a different approach towards multitasking.

To determine the number of cores your machine has, use the detectCores() function as follows.

An easy way to get started with the multicore functionality is to use the mclappy() function, which is a parallel version of lapply().

mclapply {parallel}

mclapply(X, FUN, ..., mc.preschedule = TRUE, mc.set.seed = TRUE,
         mc.silent = FALSE, mc.cores = 1L,
         mc.cleanup = TRUE, mc.allow.recursive = TRUE)


+ Recent posts