【问题标题】:Performance benefits of chaining over ANDing when filtering a data table过滤数据表时链接 ANDing 的性能优势
【发布时间】:2019-10-25 11:38:25
【问题描述】:

我习惯将类似的任务集中到一行中。例如,如果我需要对数据表中的abc 进行过滤,我会用AND 将它们放在一个[] 中。昨天,我注意到在我的特殊情况下,这非常慢,而是测试了链接过滤器。我在下面提供了一个示例。

首先,我为随机数生成器播种,加载,并创建一个虚拟数据集。

# Set RNG seed
set.seed(-1)

# Load libraries
library(data.table)

# Create data table
dt <- data.table(a = sample(1:1000, 1e7, replace = TRUE),
                 b = sample(1:1000, 1e7, replace = TRUE),
                 c = sample(1:1000, 1e7, replace = TRUE),
                 d = runif(1e7))

接下来,我定义我的方法。第一种方法将过滤器链接在一起。第二个 AND 将过滤器组合在一起。

# Chaining method
chain_filter <- function(){
  dt[a %between% c(1, 10)
     ][b %between% c(100, 110)
       ][c %between% c(750, 760)]
}

# Anding method
and_filter <- function(){
  dt[a %between% c(1, 10) & b %between% c(100, 110) & c %between% c(750, 760)]
}

在这里,我检查它们给出了相同的结果。

# Check both give same result
identical(chain_filter(), and_filter())
#> [1] TRUE

最后,我对它们进行基准测试。

# Benchmark
microbenchmark::microbenchmark(chain_filter(), and_filter())
#> Unit: milliseconds
#>            expr      min        lq      mean    median        uq       max
#>  chain_filter() 25.17734  31.24489  39.44092  37.53919  43.51588  78.12492
#>    and_filter() 92.66411 112.06136 130.92834 127.64009 149.17320 206.61777
#>  neval cld
#>    100  a 
#>    100   b

reprex package (v0.3.0) 于 2019 年 10 月 25 日创建

在这种情况下,链接可以减少大约 70% 的运行时间。为什么会这样?我的意思是,数据表的幕后发生了什么?我没有看到任何反对使用&amp; 的警告,所以我很惊讶差异如此之大。在这两种情况下,他们评估相同的条件,所以这不应该是一个区别。在 AND 情况下,&amp; 是一个快速运算符,然后它只需过滤一次数据表(即使用 AND 产生的逻辑向量),而不是在链接情况下过滤三次。

奖金问题

这个原则是否适用于一般的数据表操作?模块化任务总是更好的策略吗?

【问题讨论】:

  • 我对这个观察结果也有同感,也有同样的疑惑。根据我的经验,在一般操作中可以观察到链接速度的提升。
  • 虽然 data.tavle 确实对这种情况进行了 一些 优化(仅此一项就是一项壮举,并且与基础 R 相比有了很大的改进!),通常是 A & B & C & D 将在组合结果和过滤之前评估 所有 N 个逻辑条件时间。而链接第二个第三个和第四个逻辑调用仅评估 n 次(其中 n
  • @MichaelChirico 哇。这太令人惊讶了!我不知道为什么,但我只是假设它会像 C++ 短路一样工作
  • 跟进@MichaelChirico 的评论,您可以通过执行以下操作对向量进行类似的base 观察:chain_vec &lt;- function() { x &lt;- which(a &lt; .001); x[which(b[x] &gt; .999)] }and_vec &lt;- function() { which(a &lt; .001 &amp; b &gt; .999) }。 (其中ab 是与runif 相同长度的向量 - 我使用n = 1e7 进行这些截止)。
  • @MichaelChirico 啊,我明白了。那么,最大的区别在于,在链的每个步骤中,数据表要小得多,因此可以更快地评估条件和过滤?这就说得通了。感谢您的见解!

标签: data.table r data.table


【解决方案1】:

大多数情况下,cmets 已经给出了答案:在这种情况下,data.table 的“链接方法”比“anding 方法”更快,因为链接一个接一个地运行条件。由于每一步都会减小data.table 的大小,因此对下一步的评估就更少了。 “Anding”每次都会评估全尺寸数据的条件。

我们可以用一个例子来证明这一点:当单个步骤不减小 data.table 的大小时(即两个方法的检查条件相同):

chain_filter <- function(){
  dt[a %between% c(1, 1000) # runs evaluation but does not filter out cases
     ][b %between% c(1, 1000)
       ][c %between% c(750, 760)]
}

# Anding method
and_filter <- function(){
  dt[a %between% c(1, 1000) & b %between% c(1, 1000) & c %between% c(750, 760)]
}

使用相同的数据,但使用 bench 包,它会自动检查结果是否相同:

res <- bench::mark(
  chain = chain_filter(),
  and = and_filter()
)
summary(res)
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 chain         299ms    307ms      3.26     691MB     9.78
#> 2 and           123ms    142ms      7.18     231MB     5.39
summary(res, relative = TRUE)
#> # A tibble: 2 x 6
#>   expression   min median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <dbl>  <dbl>     <dbl>     <dbl>    <dbl>
#> 1 chain       2.43   2.16      1         2.99     1.82
#> 2 and         1      1         2.20      1        1

正如您在此处看到的那样,在这种情况下,anding 方法的速度提高了 2.43 倍。这意味着链接实际上会增加一些开销,这表明通常与操作应该更快。 除非条件是逐步减小data.table 的大小。从理论上讲,链接方法甚至可能更慢(甚至将开销放在一边),即如果条件会增加数据的大小。但实际上我认为这是不可能的,因为data.table 不允许回收逻辑向量。我认为这回答了你的奖金问题。

为了比较,我的机器上的原始功能与bench

res <- bench::mark(
  chain = chain_filter_original(),
  and = and_filter_original()
)
summary(res)
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 chain        29.6ms   30.2ms     28.5     79.5MB     7.60
#> 2 and         125.5ms  136.7ms      7.32   228.9MB     7.32
summary(res, relative = TRUE)
#> # A tibble: 2 x 6
#>   expression   min median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <dbl>  <dbl>     <dbl>     <dbl>    <dbl>
#> 1 chain       1      1         3.89      1        1.04
#> 2 and         4.25   4.52      1         2.88     1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-02
    • 2012-03-19
    • 2010-09-23
    • 2017-11-13
    • 2010-10-20
    • 2011-03-21
    相关资源
    最近更新 更多