훌륭한진 못해도 좋은 개발을 하고 싶다는 생각이 많이 든다.
좋은 코드를 짜기 위해 노력중이다.
좋은 코드에 필요한 요소들은 여러가지가 있겠지만, 지금 가장 큰 흥미를
가지고 관심가지는 것은 효율성이다.
효율성에서도 여러 가지 측면이 있겠지만 코드수행시간에 특히 민감해 하고
있다.
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)
<- data.frame(x = runif(2.6e+07), y = rep(LETTERS, each = 10000))
DF
<- as.data.table(DF)
DT setkey(DT, y)
benchmark(
$y == "D", ],
DF[DFJ("D"), ],
DT[%>% dplyr::filter(y == "D"),
DF %>% dplyr::filter(y == "D")
DT )
## 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가지이다.
data.frame
형에서 필터링data.table
형에서 필터링data.frame
형에서dplyr::filter()
를 이용한 필터링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)
<- data.frame(Key = c(1,2,4,5,6,7),
df1 Var1 = c("a", "b", "c", "c", "b", "c"), stringsAsFactors = F)
<- data.frame(Key = c(2,3,4,5,6,8),
df2 Var2 = c("x", "y", "z", "xx", "yy", "zz"), stringsAsFactors = F)
benchmark(
%>% merge(df2, by = "Key", all.x = T),
df1 %>% dplyr::left_join(df2, by = "Key"),
df1 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를 역행렬 계산하는것을 쉬워할
것이다.
이러한 특성을 이용한 것이다.
<- matrix(rnorm(1000^2), 1000)
m <- svd(m)
svd
benchmark(
solve(m),
$v %*% solve(diag(svd$d)) %*% t(svd$u)
svd )
## 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