Lovetoken

저는 개발 취향을 가진 데이터 분석가 Jr. 입니다.

Navigation
 » Home
 » About Me
 » Github

R에서 의미가 있었던 프로파일링 모음

22 Feb 2017 » R



훌륭한진 못해도 좋은 개발을 하고 싶다는 생각이 많이 든다.
좋은 코드를 짜기 위해 노력중이다.
좋은 코드에 필요한 요소들은 여러가지가 있겠지만, 지금 가장 큰 흥미를 가지고 관심가지는 것은 효율성이다.
효율성에서도 여러 가지 측면이 있겠지만 코드수행시간에 특히 민감해 하고 있다.
Profiling 을 통해 이런저런 실험들을 해보고 있고, 그 중 나름 유의미했던 실험들을 정리해 보고자 한다.

프로파일링에 사용한 도구는 rbenchmark package 의 benchmark() 함수로 통일 하였고, 대조군 코드를 통해 비교적 쉽게 받아들일 수 있도록 작성해 보았다.



lapply() VS parallel::mclapply()

“parallel” package 에 있는 mclapply() 함수는 lapply() 함수와 기능이 똑같다.
다만 mc.cores 인자를 가지고 있고 멀티코어 개수를 지정하여 병렬처리를 간편하게 수행할 수 있다.

library(parallel)

benchmark(
  lapply(1000:9999, rnorm),
  mclapply(1000:9999, rnorm, mc.cores = 4)
)
                                 test replications elapsed relative 
2 mclapply(1000:9999, rnorm, mc.c ...          100 289.162    1.000 
1                 lapply(1000:999 ...          100 464.604    1.607 

위의 실험은 표준정규분포에서 난수발생을 1000번부터 9999번까지 수행하는 것을 100번1 반복시킨 후 수행시간을 평가하는 코드이다.
mclapply() 함수로 실행한 결과가 평균 289초, lapply() 함수로 실행한 결과가 평균 464초 걸렸고 상대적으로 lapply() 를 사용한 코드가 mclapply() 로 사용한 코드보다 1.607배 많은 시간이 소요됨을 알 수 있다.

반면에 난수발생을 적게 시킬 경우 mclapply()lapply() 보다 멍청해(?) 지는 것을 볼 수 있다.

benchmark(
  lapply(10:99, rnorm),
  mclapply(10:99, rnorm, mc.cores = 4)
)
##                              test replications elapsed relative 
## 1                 lapply(10:9 ...          100   0.118    1.000 
## 2 mclapply(10:99, rnorm, mc.c ...          100   1.994   16.898 



Filtering data.table, data.frame + dplyr::filter()

이번에 수행한 프로파일링은 빵군님의 블로그 글을 보다가 추가로 생긴 궁금증에 몇 개를 더 추가해 본 경우이다.

library(data.table)
library(dplyr)

DF <- data.frame(x = runif(2.6e+07), y = rep(LETTERS, each = 10000))

DT <- as.data.table(DF)
setkey(DT, y) 

benchmark(
  DF[DF$y == "D", ],
  DT[J("D"), ],
  DF %>% dplyr::filter(y == "D"),
  DT %>% dplyr::filter(y == "D")
)
##                             test replications elapsed relative
## 2                   DT[J("D"), ]          100   2.500    1.000
## 4 DT %>% dplyr::filter(y == "D")          100  23.083    9.233
## 3 DF %>% dplyr::filter(y == "D")          100  23.248    9.299
## 1              DF[DF$y == "D", ]          100  38.298   15.319

비교대상은 4가지이다.

  1. data.frame 형에서 필터링
  2. data.table 형에서 필터링
  3. data.frame 형에서 dplyr::filter() 를 이용한 필터링
  4. data.table 형에서 dplyr::filter() 를 이용한 필터링

결과는 J표현식을 사용한 data.table 이 가장 우월하다는 것을 볼 수 있다.



sqldf package 를 통해 Join 작업을 하다가 생긴 의문

R에서 SQL 문을 통해 data.frame 형을 처리하기 위한 대표적인 패키지가 sqldf 이지 않을까?
필요에 따라서 R에서 SQL 을 통해 data.frame 을 핸들링 하는 확장성을 이용할 수 있다.

하지만 문득 이런 생각이 드는 것도 사실이다.
R에서 굳이 다른 문법(SQL)을 이용하는 것은
영국 가서 미국발음으로 영어하고, 미국 가서 영국발음으로 영어 하는 것으로 비유해 보고 싶다.

merge(), dplyr::left_join(), sqldf() 3가지 함수를 통해 left join 을 하는 예제로 프로파일링을 해보았는데
그 이후 SQL 문법을 불필요하게 남용하지 않아야 겠다는 생각을 가졌다.

library(sqldf)
library(dplyr)

df1 <- data.frame(Key = c(1,2,4,5,6,7), 
                  Var1 = c("a", "b", "c", "c", "b", "c"), stringsAsFactors = F)
df2 <- data.frame(Key = c(2,3,4,5,6,8), 
                  Var2 = c("x", "y", "z", "xx", "yy", "zz"), stringsAsFactors = F)

benchmark(
  df1 %>% merge(df2, by = "Key", all.x = T),
  df1 %>% dplyr::left_join(df2, by = "Key"),
  sqldf("SELECT df1.Key, Var1, Var2 FROM df1 LEFT JOIN df2 on df1.Key=df2.Key")
)
##                                                 test replications elapsed relative 
## 2                                     df1 %>% dp ...          100   0.033    1.000 
## 1                                     df1 %>% me ...          100   0.089    2.697 
## 3 sqldf("SELECT df1.Key, Var1, Var2 FROM df1 LEF ...          100   2.361   71.545 

sqldf() 을 통한 join 이 다른방법보다 71배 더 오래걸려 비교적 느림을 알 수 있다.



Output

위의 SQL 프로파일링과도 이어지는 컨셉이다.
냉면으로 유명한 집에서 국수를 먹고, 국수가 유명한 집에서 밥을 먹지 않는 것 처럼
각 언어의 특색에 맞는 장점들을 충분히 이용하는것이 합리적이라 생각한다.

이번 프로파일링도 비슷하다.
R에서 처리된 결과물을 R object 이미지로(.rda)로 저장하는것과 .csv 포맷으로 저장하는 것을 비교해 보았다.
(R에서 처리된 결과물의 예제를 mtcars 내장데이터로 가정해보겠다)

benchmark(
  save(mtcars, file = "mtcars.rda"),
  write.csv(mtcars, file = "mtcars.csv")
)
##                                     test replications elapsed relative
## 1      save(mtcars, file = "mtcars.rda")          100   0.054    1.000
## 2 write.csv(mtcars, file = "mtcars.csv")          100   0.116    2.148

결과는 .rda 로 객체이미지를 저장시키는 것이 2배 빠른 것을 볼 수 있다.



역행렬 계산시 특이값 분해(Singular value decomposition) 이용

(정방)행렬의 차원이 크면 클수록 역행렬을 계산할 때 컴퓨터가 힘들어 한다.
조금이라도 빠르게 계산할 수 있는 방법이 없을까 고민하다가 특이값 분해(SVD) 를 알게된 이후로 조금 개선된 방법을 찾게 되었다.

SVD 를 아주 간단하게 설명하면 행렬2 X 를 대각행렬 D 를 중앙으로 하고 3개의 행렬곱으로 나누어 준다.

X = UDVT

그리고 이렇게 분해된 행렬곱을 변환하면

X−1 = VD−1UT

이 되는 특성을 가지고 있다.
컴퓨터 입장에선 복잡한 X를 바로 역행렬 계산하는 것 보다, 부담이 적은 단위행렬 D를 역행렬 계산하는것을 쉬워할 것이다.
이러한 특성을 이용한 것이다.

m <- matrix(rnorm(1000^2), 1000)
svd <- svd(m)

benchmark(
  solve(m),
  svd$v %*% solve(diag(svd$d)) %*% t(svd$u)
)
##                                        test replications elapsed relative
## 1                                  solve(m)          100 238.183    1.194
## 2 svd$v %*% solve(diag(svd$d)) %*% t(svd$u)          100 199.452    1.000

SVD로 나눈 행렬을 이용해 역행렬 X−1 을 구하는것이 조금 빠른것을 볼 수 있다.
상대적으로 볼 때 별차이가 없어 보일수도 있지만 행렬의 차원이 크면 클수록 절대소요시간차이는 엄청날 것이다.

그런데 여기서 반론을 제기할 수 있다.
바로 특이값 분해시 코드수행속도도 포함시켜야 하지 않느냐 이다.
즉 엄밀히 말하면 svd <- svd(m) 코드 수행시간도 더해주어야 할 것이다.

system.time(svd <- svd(m))
##   user  system elapsed 
##  4.101   0.047   4.202 

하지만 4초내외로 특이값분해는 의외로 많은 시간이 걸리지 않는다.
이 4초 내외를 더해보았자 대세에 지장이 없는 수준이다.
게다가 svd() 말고도 이보다 더 빠르게 특이값 분해를 할 수 있는 방법3들도 있으니 참고해 볼만하다.



To be continued

의미 있는 프로파일링이 다음에 또 생기면 이어서 덧붙여질 예정이다.



프로파일링에 사용했던 동작환경

sessionInfo()
## R version 3.3.1 (2016-06-21)
## Platform: x86_64-apple-darwin13.4.0 (64-bit)
## Running under: OS X 10.12.3 (Sierra)
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## attached base packages:
## [1] parallel  stats     grDevices utils     datasets  graphics  methods  
## [8] base     
## 
## other attached packages:
## [1] rbenchmark_1.0.0            data.table_1.10.4          
## [3] ggplot2_2.2.1               dplyr_0.5.0                
## [5] knitr_1.15.1                useful.lovetoken_0.1.0.0090
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_0.12.9      magrittr_1.5     munsell_0.4.3    colorspace_1.3-2
##  [5] R6_2.2.0         stringr_1.2.0    plyr_1.8.4       tools_3.3.1     
##  [9] grid_3.3.1       gtable_0.2.0     pacman_0.4.1     DBI_0.5-1       
## [13] htmltools_0.3.5  yaml_2.1.14      lazyeval_0.2.0   assertthat_0.1  
## [17] digest_0.6.12    rprojroot_1.2    tibble_1.2       evaluate_0.10   
## [21] rmarkdown_1.3    stringi_1.1.2    scales_0.4.1     backports_1.0.5

  1. benchmark() 함수에서 replications 인자를 통해 조정이 가능하며, 디폴트 값이 100이다↩︎

  2. 이때 행렬은 정방행렬이 아니어도 특이값 분해가 가능하다↩︎

  3. corpcor::fast.svd() 와 같은 것도 있다↩︎