코드의 효율성은 좁게는 컴퓨터가 얼마나 효율적으로 빠르게 수행하여 시간을 줄일 수 있느냐로 계량화할 수 있다. 좀더 넓게는 개발자 생산성으로 정의된다. 즉, 컴퓨터가 수행한 시간과 함께 개발자가 투여한 시간을 함께 측정하여 계량화한다. 효율적인 R 코드 작성의 시작은 도날드 커누스(Donald Knuth)의 명언에서 시작된다.
premature optimization is the root of all evil
효율적인 R 코드를 작성하는데 필요한 3가지 구성요소는 다음과 같다.
기본적인 데이터 분석도 마찬가지로 데이터 분석을 위해서 생각(think) → 코딩(describe) → 실행(describe) 과정을 거친다. R과 C언어는 다만 강조하는 영역에서 차이가 난다. R은 생각시간이 동일할 때, 코드실행보다는 코드 작성을 줄이는데 초점이 두고 있는 반면 C언어는 코드 작성에 시간이 다소 걸리더라도 코드 실행이 빠른 면에서 장점이 있다.
R 버젼
R은 매년 4월 새로운 버젼이 출시된다. 예를 들어, 3.0, 3.1, 3.2, 3.3, … 년중으로 버그 수정을 통해 추가 버젼이 출시되는데 버젼이 3.3.0, 3.3.1, 3.3.2, …
속도와 관련된 R 버젼은 버젼 2.0 출시당시 Lazy Loading, fast loading, 버젼 2.13에서 바이트 컴파일러 지원을 통한 함수 속도 향상, 버젼 3.0 대규모 벡터지원이 포함된다.
성능측정결과는 컴퓨터마다 차이가 날 수 있다. 그래서 먼저 본인 컴퓨터 성능을 측정해 보자.
benchmarkme 팩키지에서 성능측정을 위해 개발되어 시스템에 대한 정보를 얻을 수 있는 몇가지 함수도 함께 제공한다.
컴퓨터 성능측정을 위해 크게 benchmark_std()
, benchmark_io()
두가지 함수가 제공된다.
프로그래밍, 행렬계산, 행렬함수에 대한 5가지 작업을 수행하고 성능을 평가한다. 5, 50, 200 MB csv
파일을 읽고, 쓰는 입출력 성능도 비교한다.
csv
파일 읽기csv
파일 쓰기benchmark_std
함수를 활용한 기본 성능 평가는 다음과 같다. upload_results
함수를 통해 결과를 올리게 되면, Shiny 앱으로 다른 데이터 분석개발자들의 컴퓨터도 함께 살펴볼 수 있다. CPU, 메모리 등 어떤 컴퓨터 조합으로 데이터 분석개발을 하고 있는지 엿볼 수도 있다.
Press return to get next plot
Press return to get next plot
library(microbenchmark)
팩키지를 사용하면 함수에 대한 성능을 간편하게 측정할 수 있다. 보다 자세한 사항은 R 코드 성능 측정기준 - 벤치마킹을 참조한다.
Unit: nanoseconds
expr min lq mean median uq max
colon(n) 0 342 330923.1 1194.5 2731 3297964
seq_default(n) 5462 8192 131175.0 23040.5 27649 1115478
seq_by(n) 8110764 8379735 20895133.4 9384279.0 10020866 126498838
neval
10
10
10
효율적으로 빠르게 돌아가는 R코드를 작성하는 방법은 단 한가지 방법은 존재하지 않지만, 일반적인 원칙은 존재한다.
\(1,2,3, \cdots, n\)까지 자유수 순서를 갖는 벡터를 만드는 때, 벡터를 하나씩 순차적으로 키워나가면서 루프를 돌리게 되면 최악이다. 차악으로 미리 벡터 크기를 지정하고 나서, 벡터를 순차적으로 채워나가는 것이고, 콜론 연산자를 사용하는 것이 구문도 간결하고 속도도 무척 빠르다.
Unit: microseconds
expr min lq mean median
first_method(n) 5.120 5.120 5.120 5.120
second_method(n) 7947.948 7947.948 7947.948 7947.948
third_method(n) 9395699.268 9395699.268 9395699.268 9395699.268
uq max neval
5.120 5.120 1
7947.948 7947.948 1
9395699.268 9395699.268 1
정규분포 난수를 10개 생성시키는 경우, 루프를 돌리게 되면 rnorm()
함수를 10번 호출하고, 이를 저장하기 위해서 할당도 10번 일어나게 된다. 반면 벡터 연산을 활용할 경우 rnorm()
함수를 1회 호출하고 할당도 1회하게 된다. 따라서 속도가 빠르게 된다.
더불어, 근본적으로 R코드는 하단에 C 혹은 포트란 코드로 연결되어 실행되게 되어 있어, 가능하면 빠르게 하단의 C 혹 포트란 코드로 내리는 것이 속도향상에 이르는 지름길이 된다.
Unit: milliseconds
expr min lq mean median uq
first_method(n) 69.98188 69.98188 69.98188 69.98188 69.98188
second_method(n) 1584.96966 1584.96966 1584.96966 1584.96966 1584.96966
third_method(n) 1729.62368 1729.62368 1729.62368 1729.62368 1729.62368
max neval
69.98188 1
1584.96966 1
1729.62368 1
조건이 동일하면 데이터프레임보다 행렬을 사용하는 것이 속도가 빠르다. 딥러닝 개발을 경험하게 되면 데이터프레임이 기본 자료형이 아니라 텐서 즉 행렬이 기본자료형으로 우연의 일치는 아니다.
데이터프레임은 행과 열로 구성된 구조를 갖는데, 열은 동일한 자료형이되는 반면, 행으로 보면 자료형이 다르다. 반면에 행렬은 행과 열 모두 동일한 자료형을 갖는다. 이런 점이 메모리에 데이터가 저장되는 면에서 효율성을 갖게 된다.
매우 큰 행렬과 데이터프레임을 두고 mat[,7], df[,7]
행렬과 데이터프레임으로 특정 행을 선택할 경우 속도를 비교해보면 그 차이를 확연히 알 수 있다.
고성능 R 코드 작성은 profviz
팩키지를 통해 병목점을 파악하여 앞선 개념을 적용하여 병목점의 속도를 개선한다. 예를 들어, 루프가 돌고 있는 영역이 있다면 벡터화 연산을 적용하고, apply
함수가 적용되어 있다면, rowSums()
를 동일 기능을 구현하는 등 다양한 방법으로 병목점이 있는 부분에 대해서 속도를 향상시킬 수 있다.
또다른 성능향상은 병렬화를 작업을 통해 싱글코어로 개발된 코드를 멀티코어를 활용하여 동시에 돌릴 수 있도록 코드를 개선하는 것이다.