什么情况下 `vapply()` 比 `sapply()` 慢?

Mar*_*ark 5 loops r apply lapply sapply

该函数的文档*apply()指出:

vapply与 类似sapply,但具有预先指定的返回值类型,因此使用起来更安全(有时更快)。[强调我的。]

这对我来说是有道理的,为什么它会更快 - 浪费更少的时间检查类型 - 但是,考虑到他们可以说“与”vapply()一样快或更快sapply(),但选择不这样做,我解释了他们的选择有时比他们更快潜在的意思是“对于大多数任务来说vapply(),平均速度更快,但在某些情况下,平均速度可能相同,或者在其他情况下甚至更慢 ——这对我来说似乎很奇怪。为什么它会变慢?高级 R状态 '比'vapply()更快sapply(),相比之下,这是相当明确的。

我是否误解了这一点,或者是否存在vapply()比 慢的情况sapply(),如果是的话,它们是什么?

例如,其基本原理可能是由于垃圾收集的差异,或者处理某些类型的速度,或者分配内存或其他东西(这些都是疯狂的猜测)。

我所做的研究:

令人惊讶的是,我在 StackOverflow 或其他地方找不到在线解决此问题的方法。有很多问题涉及 vapply及其安全性。在一些 比较中,虽然vapply()与 一样快或更快sapply(),但有许多迭代比最慢的vapply()迭代更快(其中一个迭代比或apply()快得多。长话短说,我有点迷失了!lapply()vapply()

您能提供的任何帮助将不胜感激!

Sam*_*amR 8

快速支票vapply()不是免费的

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

// 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));
}
Run Code Online (Sandbox Code Playgroud)

本质上,这会创建一个与输入列表长度相同的输出列表,迭代它以将每个元素设置为应用到输入的每个元素的用户提供的函数的结果。sapply()然后通过 运行结果simplify2array()

相反,C代码vapply()做了更多的工作。其中很多是优化,这使得它比 更快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));
Run Code Online (Sandbox Code Playgroud)

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

这些支票昂贵的情况

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

lapply(1, \(i) seq(1e9))
Run Code Online (Sandbox Code Playgroud)

lapply()在这里会跑得很快。seq(1e9)产生一个ALTREP,一个替代表示。这意味着它不必分配 length 的向量1e9,而是分配一个小得多的对象,该对象本质上保存起始值、结束值和增量。但是,状态文档ALTREP

对于现有的 C 代码,ALTREP 对象看起来就像普通的 R 对象。

这意味着vapply()不知道这是一个 an ALTREP,因此它需要以一种非常昂贵的方式检查长度(比仅在 R 中运行要昂贵得多length(),因为 R 知道 anALTREP是什么)。

sapply()还必须做一些代价高昂的事情。它基本上是这样做的:

simplify2array(list(seq(1e9)))
Run Code Online (Sandbox Code Playgroud)

这会创建一个包含行matrix1e9单列,即它将 计算ALTREP为标准整数向量,因此它在 RAM 中分配一个大向量。

因此vapply()sapply()两者都必须做一些比 更昂贵的事情lapply()。问题是:哪个更贵?

对人为案例进行基准测试

让我们来测试一下:

simplify2array(list(seq(1e9)))
Run Code Online (Sandbox Code Playgroud)

结果

  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.     
Run Code Online (Sandbox Code Playgroud)

我们可以在这里看到它vapply()比 慢得多sapply()。有一些警告:这些测试仅在我的电脑上进行,而且速度太慢,我只进行了三次迭代。另外,我确实需要玩一些东西才能到达这里。对于长度小于 的向量1e9vapply()比 更快sapply()

结果图

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))
    }
)
Run Code Online (Sandbox Code Playgroud)

请注意,时间采用对数刻度。

在此输入图像描述

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