R을 프로덕션 레벨에서 사용하자!

대부분 많은 사람들이 알겠지만 R은 분석언어이고, 프로덕션에션 레벨에서 사용하기 힘든 언어이다.프로덕션에서 사용하기 힘들다는건 서비스로 적용하기 힘든 프로토타이핑용 언어라는 것이다. 이런 중요한 이유중에 하나가 퍼포먼스 이슈가 있다. R언어는 상당히 많은 부분의 리소스를 데이터의 무결성 체크(NA와 같은 값들을 결정하기 위한 로직)나 분석 오류를 잡아내기 위해 할애한다. 따라서 많은 종류의 하이레벨 함수들을 사용하게 되는데, 이는 데이터 분석시 분석 실수 가능성을 낮힐 순 있으나 이를 그대로 프로덕션에 옮길경우 엄청난 퍼포먼스 저하를 가져오게 된다. 따라서 퍼포먼스 향상을 위한 아래와 같은 여러 팁들이 공유되고 있는 상황이다.

  • 벡터화 연산를 하자!(for문 사용 자제)
  • 바이트코드 컴파일러를 사용해 최적화된 실행 코드를 생성하자!
  • parallel패키지와 같은 것을 이용해 분산 처리를 하자!
  • 수치연산시 멀티코어를 활용하기 위해 blas 라이브러리를 교체한다.
  • hash, data.table과 같은 하이퍼포먼스 자료구조를 사용하자!
  • …..

등등 정말 많은 팁들이 있고 필자 역시 이를 몸소 실천하면서 어떻게하면 프로덕션에서 활용할지 고민을 많이 했다. 물론 다른 언어로 포팅을 할 수 있겠으나 여러 통계 모델링 함수를 재 구현하지 않고 사용할 수 있다는 기능적인 확장성을 포기하기가 힘들었고, 기존의 분석 코드나 로직을 다시 재생산하지 않고 사용하고 싶다는 이유때문이며, 무엇보다 원시함수를 사용하는 벡터화 연산을 하며 데이터를 핸들링 할때 그 작업 효율과 더불어 퍼포먼스는 타의 추종을 불허하기 때문이다. 하지만 몇몇 병목이 생기는 부분을 해결하기란 여간 번거로운 일이 아닐 수 없다.

사실 위 팁들에서 빠진게 하나있다. 바로 네이티브 코드로 코드를 재구현하는 부분이다. 네이티브 코드라는건 C혹은 C++로 재구현하는 것을 의미한다. 그럼 어떤 부분을 재구현 할 것인가를 생각해볼 필요가 있는데, 이런 코드 퍼포먼스 프로파일일 분석에 맞는 profr과 같은 패키지를 이용해 프로파일링 결과를 분석해 어떤 부분이 병목지점인지 파악하고 그 부분을 최적화 하는 것이다. 물론 이 작업은 위에서 이야기한 최적화를 다 한 이후에 진행이 되어야 될 것이다.

이렇게 프로파일링이 끝나서 몇몇 병목지점을 파악하면 이들 코드가 어떻게 구성되어 있는지 어떤 방식으로 작동되어 느린지를 파악하면 된다. 함수 내부를 보면 대부분 쓸데없는 체크나 검정을 하면서 느려지는 것을 확인할 수 있다.

대부분 많은 병목을 야기시키는 부분은 rbind,cbind와 같은 데이터를 계속 추가해 나가는 함수들에서 발생하곤 한다. 특히나 한줄씩 레코드를 추가해 나가는 rbind 함수는 끔찍한 결과를 초래한다. 매번 함수 호출을 할 때마다 결과 테이블을 처음부터 새로 만드는 것이다. 이런 메모리 재사용성 측면 말고도, 데이터 테이블의 자료구조로 볼때 좋지 않은 연산작업이다. 왜냐면 데이터 테이블 자료 구조는 사실 컬럼단위로 구성된 리스트 객체이기 때문이다. 따라서 이런 경우에는 리스트로 컬럼을 하나씩 추가해 나가면서 마지막 과정에서 데이터 테이블로 변환을 시켜주는게 퍼포먼스상 가장 빠른 방법이다.

아래 예는 append함수를 재구현한 케이스를 보여준다. 물론 이 예제에서 for문을 사용한건 예시를 보여주기 위함인데, 예를 들어 stdin으로 입력되는 양을 예상하지 못하는 입력 데이터를 처리할 때 아래와 같은 코드를 짜게 되는데, 메모리 사용 측면에서 효율적이지 못하고 연산 측면에서도 효과적이지 못한 코드이다.

R에서는 많은 사람들이 이런 형태의 코드를 짜게 되고 이보다 더 훨씬 나은 코드를 짤 수 있는 문법적인 장치도 거의 존재하지 않는다. 물론 입력되는 데이터의 길이를 예상할 수 있다면 효율적으로 코딩이 가능하지만 말이다.

이런 경우 STL의 vector와 같은 자료구조를 사용하면 훨씬 나은 결과를 보여준다.

system.time({
    vec <- c()
    for(i in 1:200000){
      vec <- append(vec, i)
    }
  })
##    user  system elapsed 
##  45.107   6.806  51.899


library(Rcpp)

sourceCpp(code="
#include <Rcpp.h>

std::vector<double> basevstl;

// [[Rcpp::export]]
void append_cpp(int inputs){
  basevstl.push_back(inputs);
}

// [[Rcpp::export]]
Rcpp::NumericVector getResults(){
  return Rcpp::wrap(basevstl);
}
")

system.time({
    for(i in 1:200000){
      append_cpp(i)
    }
  })
##    user  system elapsed 
##   0.475   0.065   0.539

all(getResults() == vec)
## [1] TRUE

위 코드는 간단하게 인라인상에서 코딩한 경우인데, 필요에 맞게 함수를 구성해 최소한의 연산만 가능하게끔 만들었고, 모듈이라기 보다는 스크립트 짜는 느낌으로 작성한 코드이며 범용적이라기 보다는 해당 분석 로직에 종속적인 코드라 할 수 있는데, 100배 정도 속도가 향상된 결과를 볼 수 있다.  100일 걸리는 프로세싱 타임을 하루로 줄일 수 있는 엄청난 향상이다.  (물론 이  C++ 코드는 스크립트 실행 타임에 컴파일되고 링킹되는 엄청난 장점을 가지고 있다.)

Rcpp는 필자가 여태 만나봤던 네이티브 언어 임베딩을 하는 인터페이싱들  중에서 가장 편리하다고 생각하는데, 기본적으로 STL뿐만 아니라 Boost라이브러리를 지원하는 BH패키지도 나온 상황에다가 C++관련 자료구조나 라이브러리를 직접 핸들링할 수 있게하며 기본적으로는 R의 기본 자료구조들에 대한 원시적(Premitive)접근을 가능하게 해서 성능 튜닝의 여지를 아주 편하게 제공하고 있는 엄청난 장점을 제공하고 있어 앞으로 많은 패키지들이 이를 기반으로 구현이 되지 않을까 하는 생각도 해본다.

plyrRcpp기반으로 재구현한 dplyr의 경우 10배 이상의 성능향상이 있다고 이야기되는 추세로 볼때 R의 Premitive함수를 제외하고 같은 코드를 Rcpp로 재구현 할 경우 비슷한 성능향상을 꾀할 수 있을거라 생각한다.

물론 C++로 구현할 바에야 좀더 빠른 다른 언어(Java, Perl, Python 등등)로 처음부터 모두 구현하는게 낫지 않겠느냐 할 수 있으나 중요한 로직에서 예상치 못한 계산 에러를 줄이는 체크를 언어에서 해줄 수 있는 부분들에 대한 미련(퍼포먼스가 다소 느려지더라도…)과 위에서 언급했던 기본적으로 원시 함수 형태로 제공되는 여러 통계 계산을 위한 함수들에 대한 재구현 이슈(내가 직접 구현했을때 시간적인 소모와 함수의 신뢰성을 얻기 위한 시행 착오의 짐들…)때문에 이런 방식으로 사용을 하는 것이다.

이렇듯 Rcpp는 R의 좋은 패키지나 함수들을 실무(서비스, 프로덕션)에서 부담없이 사용할 수 있게 하는 유연성을 제공해주며,  데이터 프로세싱을 위한 자료구조가 부족한 R에서 통계함수 재구현이 아닌 C++레벨에서 제공하는 원시 자료구조정도만 잘 활용하더라도 큰 효과를 볼 수 있을거라 생각한다.

 

CC BY-NC 4.0 R을 프로덕션 레벨에서 사용하자! by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.