【问题标题】:Subsetting a data.table in a for loop is slower and resource hungry在 for 循环中设置 data.table 的子集速度较慢且资源匮乏
【发布时间】:2019-06-22 16:06:47
【问题描述】:

使用data.table R 包时,我注意到在运行一个简单的for 循环时处理器使用率非常高,该循环将使用来自另一个data.table 的值对数据集进行子集化。当我说高使用率时,我的意思是在循环运行的整个过程中 100% 的所有可用线程。

有趣的是,对相同的进程使用data.frame 对象对于相同的输出所花费的时间减少了 10 倍。并且只有一个核心达到 100%。

这是我希望可重现的示例:

chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

test1 <- list()
test2 <- list()

#loop subsetting a data.frame
system.time(
for (i in 1:nrow(df2)) {
  no.dim <- dim(df1[df1$chr == df2[i, 'chr'] & df1$start >= df2[i, 'start'] & df1$end <= df2[i, 'end'], ])[1]
  test1[i] <- no.dim
})

# loop subsetting a data.table using data.table syntax
system.time(
for (i in 1:nrow(dt2)) {
  no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
  test2[i] <- no.dim
})

# is the output the same
identical(test1, test2)

这是输出:

> #loop subsetting a data.frame
> system.time(
+ for (i in 1:nrow(df2)) {
+   no.dim <- dim(df1[df1$chr == df2[i, 'chr'] & df1$start >= df2[i, 'start'] & df1$end <= df2[i, 'end'], ])[1]
+   test1[i] <- no.dim
+ })
   user  system elapsed 
  2.607   0.004   2.612 
> 
> # loop subsetting a data.table using data.table syntax
> system.time(
+ for (i in 1:nrow(dt2)) {
+   no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
+   test2[i] <- no.dim
+ })
   user  system elapsed 
192.632   0.152  24.398 
> 
> # is the output the same
> identical(test1, test2)
[1] TRUE

现在,我知道可能有多种更好、更有效的方法来执行相同的任务,而且我可能不是以data.table 的方式来做的。但假设出于某种原因,您有一个使用“data.frame”对象的脚本,并且您想快速重写该东西以使用data.table。上面采取的方法似乎完全合理。

任何人都可以重现关于减速和高处理器使用率的相同情况吗?它是否可以通过保持或多或少相同的子集过程以某种方式修复,还是必须完全重写才能在data.table's 上有效使用?

PS:刚刚在Windows机器上测试过,线程使用正常(一个线程100%运行),但还是比较慢。在与我类似的系统上对其进行了测试,结果与上述相同。

R version 3.5.1 (2018-07-02)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.10

Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.8.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.8.0

locale:
 [1] LC_CTYPE=C           LC_NUMERIC=C         LC_TIME=C            LC_COLLATE=C        
 [5] LC_MONETARY=C        LC_MESSAGES=C        LC_PAPER=et_EE.UTF-8 LC_NAME=C           
 [9] LC_ADDRESS=C         LC_TELEPHONE=C       LC_MEASUREMENT=C     LC_IDENTIFICATION=C 

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] data.table_1.12.0

loaded via a namespace (and not attached):
 [1] compiler_3.5.1   assertthat_0.2.0 cli_1.0.1        tools_3.5.1      pillar_1.3.1    
 [6] rstudioapi_0.9.0 tibble_2.0.0     crayon_1.3.4     utf8_1.1.4       fansi_0.4.0     
[11] pkgconfig_2.0.2  rlang_0.3.1   

编辑:

感谢大家的 cmets。看来减速问题与@Hugh 详述的[.data.table 的开销有关。正如@denis 所指出的,efficient subsetting of data.table with greater-than, less-than using indices 也提到了同样的问题。

@Frank 提出的修复虽然确实很有效并且产生类似的输出,但通过完全删除循环并在原始数据集中添加一个可能不需要的列来改变进程的行为。

EDIT.1:

在我第一次编辑后,@Frank 添加了另一种方法,该方法包括使用 data.table 语法计算列表列。虽然它非常整洁,但我必须承认我需要一段时间才能弄清楚发生了什么。我认为它只是在子集 data.table 的开始和结束列上计算 lm(),所以我尝试使用 for 循环和 data.frames 重现结果。时间:

> system.time({res <- dt1[dt2, on=.(chr, start >= start, end <= end), .(n = .N, my_lm = list(lm(x.start ~ x.end))), by=.EACHI][, .(n, my_lm)]; res <- as.list(res$my_lm)})
   user  system elapsed 
 11.538   0.003  11.336 
> 
> test_new <- list()
> system.time(
+   for (i in 1:20000) {
+     df_new <- df1[df1$chr == df2$chr[i] & df1$start >= df2$start[i] & df1$end <= df2$end[i],]
+     test_new[[i]] <- lm(df_new$start ~ df_new$end)
+   })
   user  system elapsed 
 12.377   0.048  12.425 
> 

只要你有一个像 lm() 这样的瓶颈函数,你最好(为了控制和可读性)使用基本的 for 循环,但使用 data.frames。

【问题讨论】:

标签: r for-loop data.table subset cpu-usage


【解决方案1】:

任何人都可以重现关于减速和高处理器使用率的相同情况吗?它是否可以通过保持或多或少相同的子集过程以某种方式修复,还是必须完全重写才能在 data.table 上有效使用?

对于 OP 的两种方法(DF 和 DT,分别),我得到 5 秒和 44 秒的时间,但是...

system.time(
  dt2[, v := dt1[.SD, on=.(chr, start >= start, end <= end), .N, by=.EACHI]$N]
)
#    user  system elapsed 
#    0.03    0.01    0.03 
identical(dt2$v, unlist(test1))
# TRUE

但是假设出于某种原因,您有一个使用“data.frame”对象的脚本,并且您想快速重写该东西以改用 data.table。上面采取的方法似乎完全合理。

一旦你习惯了 data.table 的语法,这写起来很快。


如果不想修改dt2直接取向量就行了……

res <- dt1[dt2, on=.(chr, start >= start, end <= end), .N, by=.EACHI]$N

对于此示例,行计数向量是有意义的,但如果您有更复杂的输出需要在 list 中,您可以使用 list 列...

res <- dt1[dt2, on=.(chr, start >= start, end <= end), .(
  n = .N, 
  my_lm = list(lm(x.start ~ x.end))
), by=.EACHI][, .(n, my_lm)]

       n my_lm
    1: 1  <lm>
    2: 1  <lm>
    3: 1  <lm>
    4: 1  <lm>
    5: 1  <lm>
   ---        
19996: 2  <lm>
19997: 2  <lm>
19998: 2  <lm>
19999: 2  <lm>
20000: 2  <lm>

【讨论】:

  • 我尝试了两种 OP 的方法,但我没有得到相同的时间。我确实为data.table 买了一个更长的。知道为什么吗?
  • @denis 也许 data.table 需要更长的时间,因为[ 对 data.tables 有更多开销,并且每次循环迭代使用四次..?不过,我仍然不希望这会导致如此大的差异。
  • 是的,这对我来说也很奇怪......我得到了 25.19 秒,而 3.72 秒是巨大的。顺便说一句,我喜欢你的回答。在您回答之前从未了解过EACHI 的用法。学到了很多,谢谢
  • @Frank 不错的答案!对于 OP 的方法,我得到了类似的时间安排。我也认为 [ 导致时间增加,虽然我不知道为什么......
  • 确实很好的答案!我确信还有更多data.table 方式。您认为加速是由于单独每次迭代减少了[ 调用,还是还有其他原因。此外,看起来您最终会得到不同的输出,这在更广泛的情况下可能会导致下游出现问题。有没有一种直接获得相同输出的有效方法?
【解决方案2】:

用户时间和经过时间之间的差异是在后台进行一些并行化的线索:

library(data.table)
chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

print(dim(dt1))
#> [1] 4000    3
print(dim(dt2))
#> [1] 20000     3


test1 <- list()
test2 <- list()

bench::system_time({
  for (i in 1:nrow(df2)) {
    no.dim <- dim(df1[df1$chr == df2[i, 'chr'] &
                        df1$start >= df2[i, 'start'] &
                        df1$end <= df2[i, 'end'], ])[1]
    test1[i] <- no.dim
  }
})
#> process    real 
#>  3.547s  3.549s

print(getDTthreads())
#> [1] 12

bench::system_time({
  for (i in 1:nrow(dt2)) {
    no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
    test2[i] <- no.dim
  }
})
#> process    real 
#> 83.984s 52.266s

setDTthreads(1L)
bench::system_time({
  for (i in 1:nrow(dt2)) {
    no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
    test2[i] <- no.dim
  }
})
#> process    real 
#> 30.922s 30.920s

reprex package (v0.2.1) 于 2019 年 1 月 30 日创建

但同样重要的是,您调用了 [ 20,000 次。考虑这个最小用途来证明单行表的[.data.table 的开销支配了运行时:

library(data.table)
chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

bench::system_time({
  o <- integer(nrow(df2))
  for (i in 1:nrow(df2)) {
    o[i] <- df2[i, ][[2]]
  }
})
#>   process      real 
#> 875.000ms 879.398ms

bench::system_time({
  o <- integer(nrow(dt2))
  for (i in 1:nrow(dt2)) {
    o[i] <- dt2[i, ][[2]]
  }
})
#> process    real 
#> 26.219s 13.525s

reprex package (v0.2.1) 于 2019-01-30 创建

【讨论】:

  • 感谢您的洞察力。而且由于我每次迭代调用[.data.table 4 次,这看起来很合理,这将是减速的原因。我认为可能还有某种并行化正在进行,尽管由此获得的可能会在[ 开销中丢失。
猜你喜欢
  • 2016-11-23
  • 2019-02-15
  • 1970-01-01
  • 1970-01-01
  • 2023-04-02
  • 1970-01-01
  • 1970-01-01
  • 2020-05-22
  • 1970-01-01
相关资源
最近更新 更多