分配列时,data.table速度较慢

时间:2017-04-19 21:27:52

标签: r data.table

出于某种原因,这个操作似乎显示data.table分配一个新列大约是基数R的一半。这是否有原因?

require(microbenchmark)
require(data.table)
DT = data.table(a = runif(1000000), b = rnorm(1000000))
DF = data.frame(a = runif(1000000), b = rnorm(1000000))

microbenchmark(
  DT[,keycol := seq(1,nrow(DT))],
  DF$keycol <- seq(1,nrow(DF)),
times = 2)

Unit: microseconds
expr                                   min      lq      mean    median    uq     max     neval
 DT[, `:=`(keycol, seq(1, nrow(DT)))] 901.109 901.109 921.1220 921.1220 941.135 941.135     2
 DF$keycol <- seq(1, nrow(DF))        487.844 487.844 527.1865 527.1865 566.529 566.529     2

这是我的R版本,使用data.table版本1.10.4:

> version
           _                           
platform       x86_64-w64-mingw32          
arch           x86_64                      
os             mingw32                     
system         x86_64, mingw32             
status                                     
major          3                           
minor          3.3                         
year           2017                        
month          03                          
day            06                            
svn rev        72310                       
language       R                           
version.string R version 3.3.3 (2017-03-06)
nickname       Another Canoe    

3 个答案:

答案 0 :(得分:4)

很难对以ms / us范围运行的任务进行基准测试,因为很难正确地测量实际时间,而微小的扰动会对结果产生很大的影响。

我通常不会使用任何基准测试包。我建议不要使用任何特别用于更新的引用类型操作。此外,在运行代码(使用times=100L)时,虽然我在一起运行时获得了相同的时序差异,但当我分别对DTDF代码进行基准测试时,时序或多或少相同。不知道为什么。

因此,我建议运行这样的事情:

require(data.table)
set.seed(1L)
N <- 1e6L
DT = data.table(a = runif(N), b = rnorm(N))
DF = data.frame(a = runif(N), b = rnorm(N))

runs <- 100:51
t_dt <- sapply(runs, function(k) {
  # cat("k=",k,"\n",sep="")
  DTlist <- lapply(1:k, function(x) copy(DT))

  t0 = proc.time()
  for (i in 1:k) DTlist[[i]][, keycol := seq(1, nrow(DT))]
  (proc.time()-t0)[["elapsed"]]
})

t_df <- sapply(runs, function(k) {
  # cat("k=",k,"\n",sep="")
  DFlist <- lapply(1:k, function(x) copy(DF))
  t0 = proc.time()
  for (i in 1:k) DFlist[[i]]$keycol <- seq(1, nrow(DF))
  (proc.time()-t0)[["elapsed"]]
})
  

我还保留了for循环大小变量,以便平均由于运行次数差异而可能出现的变化。我将下限保持在51,因为它似乎是一个相当大的数字,其中基准测试结果将是有意义的(更重要的是,我不会没有耐心: - ))。

我们可以直接调用C函数assign,它是通过引用更新的实际函数,以避免导致运行时的任何其他影响,包括[.data.table调用开销。这将作为基线。

t_dt_base <- sapply(runs, function(k) {
  # cat("k=",k,"\n",sep="")
  DTlist <- lapply(1:k, function(x) copy(DT))

  t0 = proc.time()
  for (i in 1:k) .Call("Cassign", DTlist[[i]], NULL, 3L, "keycol", list(seq(1, nrow(DT))), FALSE)
  (proc.time()-t0)[["elapsed"]]
})

ans <- data.table(dt=t_dt/(runs), df=t_df/(runs), dt_base=t_dt_base/(runs)) # average within runs
# fwrite(ans, "timings.csv", sep=",")
(t_mean <- sapply(ans, mean)) # average across runs
#          dt          df     dt_base 
# 0.003250907 0.002789930 0.002735729

基线运行时(从直接调用到assign)与df大致相同。但是,0.000515178与基线之间存在dt秒的差异,我们可以将其标记为[.data.table开销(可能是列表的[[访问权限) 。

使用N <- 1e7Lruns <- 10:5运行返回:

        dt         df    dt_base
0.01697659 0.01468419 0.01479067

导致0.00218592(&gt;&gt; 0.0005)的差异。在我看来,还有其他因素取决于data.table(?)的大小,似乎有助于运行时...我没有时间调查该ATM。但是,希望这有点帮助。

PS:在处理这个Q时,我发现在这样的场景中可以避免(深度)副本:

x <- 1:5
.Internal(inspect(x))
# @7fdde2a4ed20 13 INTSXP g0c3 [NAM(1)] (len=5, tl=0) 1,2,3,4,5
tracemem(x)
dt <- data.table(a=1:5, b=6:10)
dt[, c := x] # 'x' is deep copied here but should be avoided

这是因为this line导致NAM(1)递增到NAM(2)(即,两个符号现在绑定到该值)。并且data.table在内部检查它并在NAM(2)时进行深度复制。这可能是可以避免的。我会尽快提出问题。

注意:这是在R-console(来自iTerm)中运行的。 RStudio似乎默认创建NAM(2),即使对于向量,这很奇怪,我不知道为什么。但这确实意味着即使我们修复了这种情况,RStudio仍然会深层复制。

答案 1 :(得分:2)

我也对这个差异有多大印象深刻...我猜它是[.data.table

开销的错误

顺便说一下,你没有正确地进行基准测试 - 更平坦的比较不会在某些时候覆盖列,而是每次都从头开始:

set.seed(102340)

times = matrix(nrow = 500, ncol = 2)
colnames(times) = c('DT', 'DF')
for (ii in seq_len(nrow(times))) {
  DT = data.table(a = runif(1000000), b = rnorm(1000000))
  DF = data.frame(a = runif(1000000), b = rnorm(1000000))

  TT0 = get_nanotime()
  DT[ , keycol := seq(1, nrow(DT))]
  TT1 = get_nanotime()
  delDT = TT1 - TT0

  TT0 = get_nanotime()
  DF$keycol <- seq(1,nrow(DF))
  TT1 = get_nanotime()
  delDF = TT1 - TT0
  times[ii, ] = c(delDT, delDF)
}
summary(times)
 #       DT                 DF         
 # Min.   : 1617687   Min.   : 420502  
 # 1st Qu.: 2205314   1st Qu.: 447691  
 # Median : 3297872   Median : 464019  
 # Mean   : 5277059   Mean   : 594214  
 # 3rd Qu.: 4291291   3rd Qu.: 578034  
 # Max.   :75731819   Max.   :2224713 

使用seq_len(nrow(DT))代替seq(1, nrow(DT))时,两种方法都更快。

差异的一个不错的部分似乎被归结为[.data.table

的开销
set.seed(102340)

ns = as.integer(10^(1:7))
ratios = numeric(length(ns))
for (nn in seq_along(ns)) {
  times = matrix(nrow = 500L, ncol = 2L)
  for (ii in seq_len(nrow(times))) {
    DT = data.table(a = runif(ns[nn]), 
                    b = rnorm(ns[nn]))
    DF = data.frame(a = runif(ns[nn]), 
                    b = rnorm(ns[nn]))

    TT0.1 = get_nanotime()
    DT[ , keycol := seq_len(nrow(DT))]
    TT1.1 = get_nanotime()
    delDT = TT1.1 - TT0.1

    TT0.2 = get_nanotime()
    DF$keycol <- seq(1,nrow(DF))
    TT1.2 = get_nanotime()
    delDF = TT1.2 - TT0.2

    times[ii, ] = c(delDT, delDF)
  }
  ratios[nn] = median(times[ , 1L])/median(times[ , 2L])
  print(ratios)
}

plot(log10(ns), ratios, type = 'b', lwd = 3L, xaxt = 'n',
     xlab = '# Rows', ylab = 'DT time / DF time',
     main = 'Ratio of DT assignment time\nvs. DF Assignment Time')
axis(side = 1L, at = 1:7, labels = ns)
abline(h = 1, lty = 2L, col = 'red')

enter image description here

答案 2 :(得分:1)

当N变大时,时间变得相当。

require(microbenchmark)
require(data.table)
N <- 1e7
DT = data.table(a = runif(N), b = rnorm(N))
DF = data.frame(a = runif(N), b = rnorm(N))
#force(DT)

ans <- capture.output(microbenchmark(
    DT[,keycol := seq_len(.N)],
    DT$keycol <- seq_len(nrow(DT)),     #as mentioned in vignette, this is slow
    DT[["keycol"]] <- seq_len(nrow(DT)),
    DT[,"keycol"] <- seq_len(nrow(DT)),

    DF$keycol <- seq_len(nrow(DF)),
    DF[["keycol"]] <- seq_len(nrow(DF)),
    DF[,"keycol"] <- seq_len(nrow(DF)),

    times = 20L))
message(paste0("#",ans,"\n"))

#Unit: milliseconds
#                                expr      min        lq      mean    median        uq      max neval
#     DT[, `:=`(keycol, seq_len(.N))]  16.1415  16.96355  29.26518  17.35340  21.91285 232.7037    20
#      DT$keycol <- seq_len(nrow(DT)) 233.7527 291.84105 385.04133 419.14105 451.05655 469.3172    20
# DT[["keycol"]] <- seq_len(nrow(DT))  15.5652  16.41960  18.81244  16.99350  20.12640  35.2602    20
# DT[, "keycol"] <- seq_len(nrow(DT)) 134.1463 136.92965 197.58160 166.53125 206.34465 394.7461    20
#      DF$keycol <- seq_len(nrow(DF))  14.5780  16.33775  19.65723  17.04340  22.78940  39.9137    20
# DF[["keycol"]] <- seq_len(nrow(DF))  14.4700  16.11845  38.83084  16.49010  22.83845 220.2109    20
# DF[, "keycol"] <- seq_len(nrow(DF))  15.1030  16.45990  26.03781  16.97035  21.90650 137.9879    20

R规格:

sessionInfo()

#R version 3.3.2 (2016-10-31)
#Platform: x86_64-w64-mingw32/x64 (64-bit)
#Running under: Windows 7 x64 (build 7601) Service Pack 1
#
#locale:
#[1] LC_COLLATE=English_Singapore.1252  LC_CTYPE=English_Singapore.1252    LC_MONETARY=English_Singapore.1252 LC_NUMERIC=C                       LC_TIME=English_Singapore.1252    
#
#attached base packages:
#[1] stats     graphics  grDevices utils     datasets  methods   base     
#
#other attached packages:
#[1] data.table_1.10.0      microbenchmark_1.4-2.1
#
#loaded via a namespace (and not attached):
# [1] Rcpp_0.12.8            assertthat_0.1         grid_3.3.2             R6_2.2.0               plyr_1.8.4             gtable_0.2.0           magrittr_1.5           scales_0.4.1          
# [9] ggplot2_2.2.1          httr_1.2.1             lazyeval_0.2.0         rstudioapi_0.6         tools_3.3.2            munsell_0.4.3          RStudioShortKeys_0.1.0 colorspace_1.3-2      
#[17] tibble_1.2