R语言 在什么情况下`vapply()`比`sapply()`慢?

pu82cl6c  于 2023-09-27  发布在  其他
关注(0)|答案(1)|浏览(74)

*apply()函数的documentation声明:
vapply类似于sapply,但具有预先指定的返回值类型,因此使用它可以更安全(并且 * 有时 * 更快)。[强调我的]
对我来说,为什么它会更快是有道理的--检查类型浪费的时间更少--但是,考虑到他们本可以说 vapply()sapply()一样快或更快”,但选择了不这样做,我解释了他们选择 * 有时更快 *,因为他们可能意味着 *“对于大多数任务,vapply()平均更快,但在某些情况下,它的平均速度可能是相同的,或者在其他情况下甚至是“更慢的速度”--这对我来说似乎很奇怪。为什么它会变慢?高级R声明 'vapply()sapply()' 快,这是相当明确的。
我是否误解了这一点,或者在某些情况下vapply()sapply()慢,如果是这样,它们是什么?
例如,原因可能是垃圾收集的差异,或者处理某些类型的速度,或者分配内存或其他东西(这些都是胡乱猜测)。

我做过的研究:

令人惊讶的是,我在网上,在StackOverflow或其他地方都找不到解决这个问题的方法。有很多关于reference vapplyits safety的问题。在few比较中,虽然vapply()sapply()一样快或更快,但有许多迭代比最慢的vapply()迭代更快(其中apply()明显快于lapply()vapply())。长话短说,我有点迷路了!
任何你能提供的帮助将不胜感激!

af7jpaap

af7jpaap1#

vapply()快速的检查不是免费的

为了设计vapply()sapply()慢的情况,让我们看看源代码。sapply()中的大部分工作由lapply()完成。lapply()的C代码非常简单。相关部分是(我的评论):

// Allocate a vector for the output with the length of the input vector/list
ans = PROTECT(allocVector(VECSXP, n));
// Loop through input list, apply relevant function and 
// assign result to each respective element of the output list
for(int i = 0; i < n; i++) {
    defineVar(install("x"), VECTOR_ELT(list, i), rho);
    SET_VECTOR_ELT(ans, i, eval(expr, rho));
}

本质上,这创建了一个与输入列表长度相同的输出列表,迭代它以将每个元素设置为应用于输入的每个元素的用户提供的函数的结果。sapply()然后通过simplify2array()运行结果。
相反,vapply()的C代码做了更多的工作。很多这是优化,使它比sapply()更快,例如。立即分配一个原子向量作为输出,而不是分配一个列表,然后简化为一个向量。但是,它也包含以下内容:

// Check that the result is the correct length for the output vector/list
if (length(val) != commonLen)
error(_("values must be length %d,\n but FUN(X[[%d]]) result is length %d"),
        commonLen, i+1, length(val));

我们告诉vapply()输出的长度和类型。这意味着,例如,如果我们告诉vapply()输出是integer(1),它需要检查每次迭代是否产生长度为1的整数向量。

一种支票很贵的情况

创建开销检查的一种方法是返回一个值,其中检查长度的开销很大。考虑一个简单的例子:

lapply(1, \(i) seq(1e9))

lapply()将在这里运行得非常快。seq(1e9)产生ALTREPalternate representation。这意味着它不必分配一个长度为1e9的向量,而是分配一个小得多的对象,该对象基本上包含起始值,结束值和增量。然而,ALTREP的文档指出:
对于现有的C代码,ALTREP对象看起来像普通的R对象。
这意味着vapply()不知道这是一个ALTREP,因此它需要以非常昂贵的方式检查长度(比在R中运行length()要昂贵得多,因为R知道ALTREP是什么)。
sapply()也必须做一些代价高昂的事情。它基本上是这样做的:

simplify2array(list(seq(1e9)))

这将创建一列matrix1e9行,即它将ALTREP计算为标准整数向量,因此它在RAM中分配一个大向量。
所以vapply()sapply()都必须做一些比lapply()昂贵得多的事情。问题是:哪个更贵?

对伪用例进行对标

我们来测试一下

results <- bench::mark(
    min_iterations = 3,
    max_iterations = 100,
    check = FALSE,
    time_unit = "s",
    lapply = {
        lapply(1, \(i) seq(1e9))
    },
    sapply = {
        sapply(1, \(i) seq(1e9))
    },
    vapply = {
        vapply(1, \(i) seq(1e9), numeric(1e9))
    }
)

结果

expression         min     median  `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr>       <dbl>      <dbl>      <dbl> <bch:byt>    <dbl> <int> <dbl>      <dbl>
1 lapply      0.00000954  0.0000277 31233.            0B   0        100     0    0.00320
2 sapply     23.3        27.9           0.0309    11.2GB   0.0309     3     3   97.0    
3 vapply     71.8        79.6           0.0126    22.4GB   0.0251     3     6  239.

我们可以在这里看到vapply()sapply()慢得多。有一些警告:这些测试只在我的PC上进行,而且速度太慢了,我只做了三次迭代。而且,我确实不得不做一些游戏来到达这里。当向量的长度小于1e9时,vapply()sapply()快。

结果图

ggplot2::autoplot(results) +
    labs(title = "Comparison of results", y = "Time (log scale)", x = "Expression")

请注意,时间是对数标度。

值得指出的是,尽管设计这种情况很有趣,但这并不典型。在绝大多数使用R的任务中,vapply()可能比sapply()快得多。此外,如您所知,还有其他好处,例如vapply()确保返回类型得到保证。

相关问题