vignettes/Performance.Rmd
Performance.RmdOne goal of the {svTidy} package is to provide an interface that is similar to {dplyr} and {tidyr} to performant code (both for speed and memory use), possibly using the {data.table} or {collapse} packages under the hood.
In this document, we compare speed and memory use of {svTidy} with {dplyr} and {tidyr}. Keep in mind that most benchmarks (including the present ones) are artificial and do not necessarily reflect real use cases. Test with your own data. Also, do you really need faster or more memory efficient code? It depends on your particular context! Also take your hardware into account (number of CPU and amount of RAM).
We test {svTidy} function both in standard evaluation (SE) and non standard evaluation (NSE) modes: the later one usually requires more computing time, but it is more convenient to write and read and closer to {dplyr}/{tidyr} syntax.
Preparation: using 3/4 of available cores for parallel code in {data.table} and {collapse}.
data.table::setDTthreads(percent = 75)
(.nthreads <- data.table::getDTthreads())
#> [1] 3
options(collapse_nthreads = .nthreads)
options(collapse_na.rm = FALSE)
options(collapse_mask = "all")Small and medium data sets.
# Small one
data(mtcars)
mtcars <- as_tibble(mtcars, rownames = "model")
mtcars_dt <- as.data.table(mtcars)
# Medium one
data(babynames, package = 'babynames')
babynames <- as_tibble(babynames)
babynames_dt <- as.data.table(babynames)Here is a couple of examples of fast and memory efficient {svTidy} functions.
filter_()
Small data set.
# Note: collapse::qDF() = quickly convert to a data.frame, for identical results
bench::mark(
dplyr = filter(mtcars, mpg > 20) |> qDF(),
data.table = mtcars_dt[mpg > 20] |> qDF(),
svTidyNSE = filter_(mtcars, ~mpg > 20) |> qDF(),
svTidySE = filter_(mtcars, mtcars$mpg > 20) |> qDF())
#> # A tibble: 4 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 551.4µs 603µs 1635. 1.36MB 8.59
#> 2 data.table 85.9µs 94.4µs 10277. 1.81MB 8.52
#> 3 svTidyNSE 40.4µs 44.9µs 21697. 154.44KB 10.9
#> 4 svTidySE 35.6µs 39.6µs 24670. 43.45KB 9.87Medium data set.
bench::mark(
dplyr = filter(babynames, n > 1000) |> qDF(),
data.table = babynames_dt[n > 1000] |> qDF(),
svTidyNSE = filter_(babynames, ~n > 1000) |> qDF(),
svTidySE = filter_(babynames, babynames$n > 1000) |> qDF())
#> # A tibble: 4 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 12.5ms 12.62ms 78.1 31.4MB 48.1
#> 2 data.table 6.44ms 8.23ms 119. 16.7MB 29.0
#> 3 svTidyNSE 5.41ms 5.59ms 173. 16.7MB 43.1
#> 4 svTidySE 5.55ms 5.67ms 165. 16.7MB 27.1arrange_()
bench::mark(
dplyr = arrange(mtcars, cyl, desc(vs)) |> qDF(),
data.table = mtcars_dt[order(cyl, -vs)] |> qDF(),
svTidyNSE = arrange_(mtcars, ~cyl, ~ -vs) |> qDF(),
svTidySE = arrange_(mtcars, 'cyl', '-vs') |> qDF())
#> # A tibble: 4 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 1.94ms 2.05ms 483. 539.3KB 8.66
#> 2 data.table 126.93µs 134.32µs 7247. 38.9KB 8.57
#> 3 svTidyNSE 79µs 85.25µs 11379. 92.9KB 11.0
#> 4 svTidySE 47.45µs 51.29µs 18917. 85.1KB 10.9
bench::mark(
dplyr = arrange(babynames, sex, desc(n)) |> qDF(),
data.table = babynames_dt[order(sex, -n)] |> qDF(),
svTidyNSE = arrange_(babynames, ~sex, ~ -n) |> qDF(),
svTidySE = arrange_(babynames, 'sex', '-n') |> qDF())
#> # A tibble: 4 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 69.9ms 69.9ms 14.3 112.7MB 85.8
#> 2 data.table 69.4ms 70.1ms 14.1 73.4MB 18.8
#> 3 svTidyNSE 27.1ms 27.8ms 34.0 73.4MB 22.7
#> 4 svTidySE 26.5ms 27ms 36.4 73.4MB 16.6Not all {svTidy} functions are currently faster or more memory efficient than their {dplyr} or {tidyr} counterparts. Those still need refactoring to be optimized. Here are some examples.
bind_rows_()
df1 <- tibble(x = 1:2, y = letters[1:2])
df1_dt <- as.data.table(df1)
bench::mark(
dplyr = bind_rows(df1, df1) |> qDF(),
base = rbind(df1, df1) |> qDF(),
data.table = rbindlist(list(df1_dt, df1_dt)) |> qDF(),
svTidy = bind_rows_(df1, df1) |> qDF(),
svTidy2 = bind_rows_(list(df1, df1)) |> qDF())
#> # A tibble: 5 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 69.9µs 75.4µs 12944. 96.4KB 11.2
#> 2 base 61µs 65.8µs 14833. 47.5KB 10.7
#> 3 data.table 96.2µs 101.4µs 9607. 121.5KB 6.28
#> 4 svTidy 602.5µs 640.9µs 1541. 207.8KB 6.37
#> 5 svTidy2 599.9µs 641µs 1536. 55.8KB 8.60
bench::mark(
dplyr = bind_rows(babynames, babynames) |> qDF(),
base = rbind(babynames, babynames) |> qDF(),
data.table = rbindlist(list(babynames_dt, babynames_dt)) |> qDF(),
svTidy = bind_rows_(babynames, babynames) |> qDF(),
svTidy2 = bind_rows_(list(babynames, babynames)) |> qDF())
#> Warning: Some expressions had a GC in every iteration; so filtering is
#> disabled.
#> # A tibble: 5 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 82.45ms 87.94ms 8.83 132MB 3.53
#> 2 base 173.98ms 291.87ms 3.43 256MB 1.71
#> 3 data.table 42.81ms 50.38ms 17.4 132MB 5.79
#> 4 svTidy 1.57s 1.57s 0.638 368MB 0.638
#> 5 svTidy2 1.51s 1.51s 0.660 338MB 0.660bind_cols_()
df1 <- tibble(x = 1:2, y = letters[1:2])
df2 <- tibble(z = 10:11, w = factor(5:6))
df1_dt <- as.data.table(df1)
df2_dt <- as.data.table(df2)
bench::mark(check = FALSE,
dplyr = bind_cols(df1, df2) |> qDF(),
base = cbind(df1, df2) |> qDF(),
data.table = cbind(df1_dt, df2_dt) |> qDF(),
svTidy = bind_cols_(df1, df2) |> qDF())
#> # A tibble: 4 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 dplyr 110µs 121µs 8095. 32.87KB 2.09
#> 2 base 149µs 161µs 6092. 2.19KB 4.36
#> 3 data.table 136µs 147µs 6703. 187.32KB 2.09
#> 4 svTidy 116µs 127µs 7739. 13.88KB 4.37