为什么“逻辑”类型的子集比“数字”类型的子集慢?

时间:2013-07-07 09:19:47

标签: r subset

假设我们有一个vector(或data.frame),如下所示:

set.seed(1)
x <- sample(10, 1e6, TRUE)

有人希望得到x的所有值x > 4,说:

a1 <- x[x > 4] # (or) 
a2 <- x[which(x > 4)]

identical(a1, a2) # TRUE

我认为大多数人会更喜欢x[x > 4]。但令人惊讶的是(至少对我而言),使用which进行子集化的速度更快!

require(microbenchmark)
microbenchmark(x[x > 4], x[which(x > 4)], times = 100)

Unit: milliseconds
            expr      min       lq   median       uq       max neval
        x[x > 4] 56.59467 57.70877 58.54111 59.94623 104.51472   100
 x[which(x > 4)] 26.62217 27.64490 28.31413 29.97908  99.68973   100

它比我快2.1倍。

我认为,差异的一种可能性可能是由于which不考虑NA>也会回复它们。但是逻辑运算本身应该是造成这种差异的原因,而不是的情况(显然)。那就是:

microbenchmark(x > 4, which(x > 4), times = 100)

Unit: milliseconds
         expr       min       lq   median       uq      max neval
        x > 4  8.182576 10.06163 12.68847 14.64203 60.83536   100
 which(x > 4) 18.579746 19.94923 21.43004 23.75860 64.20152   100

在子集化之前使用which大约慢1.7倍。但是which似乎在子集化过程中大幅度地追赶。

似乎无法使用我常用的选择武器debugoncethanks to @GavinSimpson)作为which来电.Internal(which(x)),而==来电.Primitive("==")

因此,我的问题是[类型numeric导致which类型>比{{1}}生成的逻辑向量更快?有什么想法吗?

3 个答案:

答案 0 :(得分:4)

我想我应该退出评论并添加答案。这是我的预感,建立在其他人已经回答和讨论的内容上。 (我确信在subset_dflt的C源代码中存在真正的答案。)

我有一个向量x和一个逻辑向量x > 0,我可以通过两种方式在x上对x > 0进行分组。我可以使用which或者我可以直接使用向量x > 0作为索引。但是,我们必须注意,这两者并不相同,因为x[x > 0]将保留NA,而x[which(x > 0)]则不会。{/ p>

但是,在任何一种方法中,我都需要检查向量x > 0的每个元素。在显式的which调用中,我将只检查元素的布尔状态,而在直接子设置操作中,我将不得不检查每个元素的缺失和布尔状态。

@flodel带来了一个有趣的观察。由于[is.nawhich|都是原语或内部例程,因此我们假设没有非常的开销并执行此实验:

microbenchmark(which(x > 0), x[which(x > 0)], x > 0 | is.na(x), x[x > 0],
               unit="us", times=1000)

Unit: microseconds
             expr      min       lq   median       uq      max neval
     which(x > 0) 1219.274 1238.693 1261.439 1900.871 23085.57  1000
  x[which(x > 0)] 1554.857 1592.543 1974.370 2339.238 23816.99  1000
 x > 0 | is.na(x) 3439.191 3459.296 3770.260 4194.474 25234.70  1000
         x[x > 0] 3838.455 3876.816 4267.261 4621.544 25734.53  1000

考虑到中值,我们可以看到,假设x > 0 | is.na(x)是我所说的在逻辑子设置中发生的粗略模型,那么'子集'中的实际时间是~500 us。而'子集'所用的时间约为700 us。这两个数字都是可比较的,并且表明它不是“子集”本身,这在一种方法或另一种方法中是昂贵的。相反,在which方法中计算所需的子集的工作正在进行中。

答案 1 :(得分:3)

这似乎是因为逻辑矢量的子集比数值索引的子集慢。

> ii <- x > 4
> ij <- which(x > 4)
> 
> head(ii)
[1] FALSE FALSE  TRUE  TRUE FALSE  TRUE
> head(ij)
[1] 3 4 6 7 8 9
>
> microbenchmark(x[ii], x[ij], times = 100)
Unit: milliseconds
  expr       min       lq    median        uq      max neval
 x[ii] 25.574977 26.15414 28.299858 31.080903 82.04686   100
 x[ij]  3.037134  3.31821  3.670096  7.516761 12.39738   100

更新:

可能一个原因是,索引数字的较小长度可以减少子集的(内部)循环,并导致较慢的评估。你可以找到ik&lt; ij&lt; il

但是会有另一个区别,因为iiil之间存在巨大差异。

> ii <- x > 4
> 
> ij <- which(x > 4)
> ik <- which(x > 9)
> il <- which(x > -1)
> 
> microbenchmark(x[ii], x[ij], x[ik], x[il], times = 100)
Unit: microseconds
  expr       min         lq    median        uq       max neval
 x[ii] 25645.621 25986.2720 28466.412 30693.158 79582.484   100
 x[ij]  3111.974  3281.8280  3477.627  6142.216 55076.121   100
 x[ik]   585.723   628.2125   650.184   682.888  7551.084   100
 x[il]  5266.032  5773.9015  9073.614 10583.312 15113.791   100

答案 2 :(得分:3)

这是我对它的看法。对数字进行子集可以精确地提取所需的元素。对逻辑进行子集需要检查索引向量的每个元素以查看它是否为TRUE,然后构建目标向量所需元素的内部列表。涉及两个步骤,因此需要更长的时间。

差异最大的是提取的元素数量相对于原始向量的大小而言较小。例如:

> z <- rnorm(1e8)
> system.time(z[which(z < -5)])
   user  system elapsed 
   0.58    0.03    0.60 
> system.time(z[z < -5])
   user  system elapsed 
   2.56    0.14    2.70 
> system.time(z[which(z < 5)])
   user  system elapsed 
   1.39    0.30    1.68 
> system.time(z[z < 5])
   user  system elapsed 
   2.82    0.44    3.26 

在这里,如果你只拔出一小部分元素(在我的测试中有23个元素z <-5),与逻辑索引相比,使用which只需要很小的比例。但是,如果您提取大部分元素,则时间会更接近。