【问题标题】:data.table performance using .SD with by使用 .SD 和 by 的 data.table 性能
【发布时间】:2023-07-08 05:16:01
【问题描述】:

我想按组过滤一个大的data.table。我可以使用.SD.I,虽然我个人认为前者更容易阅读,但后者更快/使用更少的内存(尽管使用.SDcols)。

在某种程度上,我很清楚为什么。对于.I,我们只需要每组一个向量,而对于.SD,我们需要一个完整的data.table。但我认为通过提供有意义的.SDcol 参数,我可以加快/节省一些内存。

但是,基准测试表明.SD 方法慢了大约 60 倍,并且占用了 300 倍的内存。当然,一个 4 列的 .SD data.table 将需要超过向量大小的 4 倍。但是速度慢了 60 倍,内存增加了 300 倍?有人可以启发我,为什么.SD 方法会占用这么多内存,因此速度要慢得多?有没有一种方法可以加快.SD 方法的速度,或者是回退到.I 方法的唯一选择?

数据设置

library(data.table)
## data set up

nr <- 1e6
nc <- 100
grp_perc <- .8
DT <- data.table(ids = sample(paste0("id", 
                                     seq(1, round(grp_perc * nr, 0))),
                              nr, TRUE))
cols <- paste("col", seq(1, nc), sep = "_")
DT[, (cols) := replicate(nc, sample(nr), simplify = FALSE)]

基准测试

results <- bench::mark(.I = DT[DT[, .(row_id = .I[which.min(col_1)]), 
                                  by = ids]$row_id, c("ids", cols[1:3]), with = FALSE],
                       .SD = DT[, .SD[which.min(col_1)], 
                                by = ids, .SDcols = cols[1:3]], 
                       iterations = 1, filter_gc = FALSE)

summary(results)
# A tibble: 2 x 13
  expression     min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result         memory         time   gc       
  <bch:expr> <bch:t> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>         <list>         <list> <list>   
1 .I           2.64s   2.64s   0.378      34.4MB    0         1     0      2.64s <df[,4] [571,~ <df[,3] [1,41~ <bch:~ <tibble ~
2 .SD          2.73m   2.73m   0.00612     9.1GB    0.342     1    56      2.73m <df[,4] [571,~ <df[,3] [2,40~ <bch:~ <tibble ~

【问题讨论】:

  • 我的理解是 .SD 方法较慢,因为它会导致[.data.table 的多个方法分派。请参阅此Github issue 和此SO answer
  • 非常有见地。谢谢,这很好地解释了这个问题,我认为对于类似的问题,我会在详细模式下查看更多关于 data.table 内部的信息。

标签: r data.table microbenchmark


【解决方案1】:

对于此特定示例,这是一种比 .I 更快的方法。请注意,这也会更改可能不适合您的顺序。

DT[order(col_1), .SD[1L], by = ids, .SDcols = cols[1:3]]

正如@Ian Campbell 提到的,这是一个 Github 问题。好消息是有一些优化,其中之一是.SD[1L]。优化是子集全部在 C 中完成,这使得它非常快。

以下是包含 @sindri_baldur 解决方案但删除了您最初的 .SD 尝试的基准测试 - 我不想等待 3 分钟 :)。

# A tibble: 3 x 13
  expression    min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 .I          4.54s  4.54s    0.220       30MB    0.880     1     4      4.54s
2 self_join  11.32s 11.32s    0.0883    76.3MB    0         1     0     11.32s
3 use_order   3.55s  3.55s    0.282     58.3MB    0         1     0      3.55s

## show that it's equal but re-ordered:
all.equal(DT[DT[, .(row_id = .I[which.min(col_1)]), 
                by = ids]$row_id, c("ids", cols[1:3]), with = FALSE][order(col_1)],
          DT[order(col_1), .SD[1L], by = ids, .SDcols = cols[1:3]])

## [1] TRUE

【讨论】:

  • 漂亮!但是,您需要对data.table 的内部有相当深入的了解才能理解.SD[which.min(col_1)].SD[1L] 慢得多,但是@[Ian Campbell] 发布的链接非常清楚。
  • 我认为您对订单的关注是不必要的,因为无论如何我都在汇总数据 - 所以原始订单在汇总表上毫无意义。
【解决方案2】:

这是一种仍然使用 .SD 的更快方法。

DT[DT[, .(col_1 = min(col_1)), by = ids], 
   on = .(ids, col_1), 
   .SD, .SDcols = c("ids", cols[1:3])]

【讨论】:

  • 不是专家,但经验告诉我,在使用 by 时最好保持简单并依赖优化的功能。
  • 谢谢+1。我几乎不习惯使用(自我)连接,并且绝对应该更频繁地使用它们。
最近更新 更多