【问题标题】:In R, why is v[length(v)+1] = x better than c(v, x)?在 R 中,为什么 v[length(v)+1] = x 比 c(v, x) 好?
【发布时间】:2017-01-13 16:34:18
【问题描述】:

我写了以下两个函数,一个使用连接来测试增加的向量大小,一个使用括号:

c_test <- function(n) {
  cv = c()
  for(i in 1:n) cv = c(cv, i)
  cv
  }

b_test <- function(n) {
  bv = c()
  for (i in 1:n) bv[i] = i
  bv
  }

library(microbenchmark)
microbenchmark(c_test(1e+4), b_test(1e+4), times = 100)

#Unit: milliseconds
#          expr       min        lq      mean    median        uq      max neval
# c_test(10000) 140.27923 145.73282 156.82319 148.16175 151.74713 267.2393   100
# b_test(10000)  49.58033  54.42992  56.24268  54.86033  56.30862 132.8394   100

这是一个很大的时间差异,我不明白为什么使用括号比使用连接好得多。在这两种情况下,分配新内存似乎都需要时间,但这似乎不是真的。我还认为c(v, x) 可能在合并之前将x 转换为与v 相同的类型,但说v[i] = as.vector(x) 并不是一个重要的时间成本。

【问题讨论】:

  • 如果需要迭代(通常不需要 -- 向量化),那么最好预先分配和填充 bv = integer(my_len); ... bv[i] = i 或者让 R 在迭代期间通过 lapply() / @987654328 管理内存@等
  • 你问错问题了。即使我们不考虑矢量化,这两种方法都非常低效。预分配,预分配,预分配。不,真的,预先分配。
  • 不,它不是预分配的。
  • 我相信区别在于 S3 方法调度的开销,但我在手机上无法检查。尝试使用 c.default。
  • 随着大小的增加,对象的增长会变得更加昂贵。

标签: r


【解决方案1】:

这可能应该是一条评论,因为我不知道实际答案,但它太长了。

“c”和“[”都是原始的、内部的和通用的。这意味着方法分派是由 C 函数完成的,这就是我在回答您的实际问题时所能得到的。那里发生了一些神秘的事情,“[”在这方面比“c”更有效。

但是,我确实想指出,根据某些但不是所有的 cmets,这两种方法都是低效的,而不仅仅是因为矢量化。为您期望的向量大小预先分配内存空间确实有很大帮助,比c[ 之间的差异要大得多。与[ 版本相比,预分配使您的速度提高了 70% 到 90%:

# very poor - repeated calls to c() to extend
c_test <- function(n) {
  cv = c()
  for(i in 1:n) cv = c(cv, i)
  cv
}

# slightly better - just use []
b_test <- function(n) {
  bv = c()
  for (i in 1:n) bv[i] = i
  bv
}

# much better practice - preallocate length of the vector
d_test <- function(n) {
  bv = numeric(n)
  for (i in 1:n) bv[i] = i
  bv
}

# good practice if possible - vectorisation
e_test <- function(n) {
  bv = 1:n
  bv
}


library(microbenchmark)
microbenchmark(c_test(1e+4), b_test(1e+4), d_test(1e+4), e_test(1e+4), times = 100)

这给出了:

Unit: microseconds
          expr        min         lq         mean     median         uq        max neval cld
 c_test(10000) 102355.753 111202.568 129250.53638 114237.234 132468.938 220005.926   100   c
 b_test(10000)  47337.481  52820.938  77029.01728  59450.864 116529.185 192643.555   100  b 
 d_test(10000)   6761.877   7492.741   7965.37288   7814.519   8353.778  11007.605   100 a  
 e_test(10000)      3.555      6.321      9.32347      8.692     10.272     27.259   100 a  

此外,正如@Roland 所说,“随着大小的增加,对象的增长会变得更加昂贵”。随着向量变大,内存中可用于放置它的位置越来越少。

我很欣赏e_test(矢量化)不适用于您的斐波那契用例,但无论如何都将其留作比较,以便了解可能矢量化时的加速规模。

【讨论】:

  • 如果您查看code of do_c,您会看到一条注释,即所有参数都已评估并检查方法调度。对于子集,检查要简单得多,因为只需要检查一个参数。此外,我反对 OP 的断言,即他们不能在他们的用例中预先分配。您总是可以猜测大小并根据需要分块增长。我最近编写了一个 OP 可能感兴趣的斐波那契实现:stackoverflow.com/a/41323414/1412059
  • 谢谢@Roland,re do_c 非常有用。我真的同意预先分配。如果你有一个 for 循环,你显然可以做到。如果是一个while循环你还是可以猜到的。
  • @Roland 如果我给人的印象是预分配不可能在这里发生,我不是故意这么说的。我个人大部分时间都避免预先分配,但只是出于愚蠢的原因(我知道可耻)。我刚开始使用 R,所以你的斐波那契代码很有用,因为我只是想知道如何实现记忆化,而且看到函数可以嵌套更有趣。
猜你喜欢
  • 2019-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多