R 에서 병렬처리하는 법

R 에서 병렬처리하는 법

개요

R 이 속도 때문에 쓰는 언어는 아니지만, 빠른 속도가 필요할 때도 분명히 있을 것이다. 코드를 깔끔하게 잘 짜더라도 너무 오래 걸린다면 보통 병렬처리나 GPU를 동원하게 된다. 언뜻 생각했을 때 R 에서 병렬처리를 할 일이 뭐 있나 싶겠지만, 빅데이터를 다루게 되거나 규모가 큰 시뮬레이션을 하게 된다면 병렬처리가 특히 유용한 수단이 된다. 오히려 R 이야말로 병렬처리를 정말 많이 사용한다고 볼 수도 있다.

코드

다음은 큰 수들을 1000개 뽑아서 소인수분해를 하고 그 시간을 재는 R 코드다:

library(foreach)
library(doParallel)

eratosthenes<-function(n){
  residue<-2:n
  while(n %in% residue){
    p<-residue[1]
    residue<-residue[as.logical(residue%%p)]
  }
  return(p)
}
  
  set.seed(150421)
  test<-sample(2*1:10^5+1,1000)
  
  system.time({
    for(n in test){
      eratosthenes(n)
    }
  })
  
  numCores <- detectCores() -1
  myCluster <- makeCluster(numCores)
  registerDoParallel(myCluster)
  
  record<-numeric(0)
  clusterExport(myCluster, "record")
  
  system.time({
  foreach(n = test, .combine = c) %dopar% {
    eratosthenes(n)
  }
  })
  stopCluster(myCluster)

20190830\_165312.png

설명

소인수분해의 경우 병렬처리의 이점을 잘 살리는 유형의 문제가 아닌데, 그럼에도 불구하고 시간을 절반이상 단축시킨 것을 볼 수 있다. 참고로 논리 프로세서가 8개라고 해서 실행시간이 1/8로 줄어들지는 않는다. 긍정적으로 말해서, 굳이 병렬처리를 하지 않아도 CPU는 원래 나름 효율적으로 일을 분배하기 때문이다. 부정적으로 말해서, 병렬처리를 해봤자 늘 쓰던 CPU라는 점에선 다른게 없기 때문이다. 물론 긍정적이든 부정적이든 병렬처리가 주는 이점은 상당하다. 다음은 각각 그냥 반복문과 병렬처리를 했을 때의 CPU 사용률을 움짤로 찐 것이다:

Honeycam2019-08-3016-08-26.gif

위의 움짤은 모든 논리 프로세서가 놀고 있다가 반복문을 돌렸을 때의 CPU 사용률을 나타낸다. 모든 논리 프로세서를 놀리는 것은 아니지만, 기본적으로 일하는 프로세서만 일하고 그 외에는 논다. Honeycam2019-08-3016-14-37.gif

위의 움짤은 모든 논리 프로세서가 놀고 있다가 병렬처리를 했을 때의 CPU 사용률을 나타낸다. 일반 반복문에 비해 병렬처리를 하면 얄짤없이 모든 프로세서가 100%의 사용률을 보이는 것을 확인할 수 있다. 이것이 실제로 실행 시간을 얼마나 단축하든, 최선을 다했다는 점은 변함이 없다.

사용되는 패키지는 foreachdoParallel로, 다음과 같은 함수들을 사용한다:

  1. detectCores(): 현재 사용하는 컴퓨터의 논리 프로세서가 몇개인지 찾아서 반환한다. 예제코드에서 -1로 코어 하나를 빼는 이유는 병렬처리에 모든 프로세서를 할당해버렸을 경우 아예 컴퓨터를 사용하는게 불가능하기 때문이다. 작업을 시켜놓은 컴퓨터로 많은 걸 하려고 하진 않겠지만, 현실적으로 최소한 마우스 커서 정도는 움직일 수 있고 진행 상황이 어느정도인지 확인하는 정도는 할 수 있는 게 좋다. 그래서 극한의 효율을 추구하는 게 아닌 이상, 프로세서 하나 정도는 여유를 준다. 이렇게 여유를 주지 않으면 화면이 전혀 움직이지 않을 때 그냥 계산이 많아서 버벅대는건지, 컴퓨터 자체가 뻗어서 움직이지 않는 것인지 알 수가 없다.
  2. makeCluster(): 이름 그대로 클러스터를 만들어주는 함수다. 그냥 메모리를 할당하는 정도의 개념으로 받아들여도 무방하다.
  3. registerDoParallel(): 만들어진 클러스터에서 병렬처리를 할 수 있도록 할당해주는 함수다.
  4. clusterExport(): 클러스터에서 병렬처리해서 얻은 데이터를 받아줄 변수를 지정해준다. 위 예시의 경우에는 mycluster에서 얻은 데이터를 record 변수에 할당하도록 한다. 문법적으로는 어색하지만 익숙해지면 별로 이상할 것도 없다.
  5. foreach(): R 이나 파이썬 외의 언어에서는 쉽게 만날 수 있는데, 적어도 병렬처리를 할 때는 조금 다른 문법을 가진다. n = test는 R 문법으로 봤을 때 n in test와 비슷한 역할을 한다. combine 옵션은 그렇게 얻은 데이터를 어떻게 취합할 것인가를 결정한다. combine=c는 그저 계산이 되는대로 저장하는 방식이다. c()가 원래 벡터를 만드는 함수기 때문이다. 원한다면 c() 를 쓰지 않고 순서대로 저장되는 옵션같은 것도 선택할 수 있지만, 이 경우 성능 저하가 유발되므로 가능한 한 처음부터 그런 코드를 짜지 않는 편이 좋다.
  6. %dopar%: 적어도 병렬처리를 할 때 있어서는 그냥 문법적인 요소로 생각하는 것이 좋다. foreach()로 정한 반복문을 어떻게 실행할지를 정하는 함수로 보면 충분하다.
  7. stopCluster(): 클러스터를 멈춰주는 함수다. makeCluster() 가 메모리를 할당한다면, stopCluster()는 해제시켜주는 역할을 한다. 컴퓨터를 어느정도 이해한다면 이것이 얼마나 중요한지 어렵지 않게 알 수 있을 것이다. 이해하지 못한다고 해도 병렬처리를 해야할만큼의 작업을 하고 있다면 이러한 함수가 왜 있는지는 쉽게 납득할 수 있을 것이다.

재미있는 사실은 위의 함수가 정확히 어떤 역할들을 하는지 몰라도 상관없다는 점이다. 그냥 복붙해서 필요한 부분만 바꾸고, foreach() 함수의 옵션만 정확하게 파악하면 된다. 백 번 읽는 것보다 한 번 실행해보는 것이 낫다. 정말로 필요한 입장이라면 함수 하나하나에 의미를 두지 말고 그냥 실행해보자.

댓글