为什么将这个循环包装在函数中可以将速度提高 8 倍?

crf*_*crf 9 for-loop r lapply

我试图更深入地了解 R 中的循环与 *apply 函数。在这里,我做了一个实验,用 3 种不同的方式计算前 10,000 个三角形数。

  1. unwrapped:一个简单的for循环
  2. wrapped:我采用与之前完全相同的循环,但将其包装在一个函数中。
  3. vapply:使用vapply匿名函数。

结果在两个方面让我感到惊讶。

  1. 为什么wrapped比 (?!?!) 快 8 倍,unwrapped我的直觉是给定wrapped实际上做了更多的事情(定义一个函数然后调用它),它应该更慢。
  2. 为什么它们都比 vapply 快得多?我希望 vapply 能够进行某种优化,其性能至少与循环一样好。
microbenchmark::microbenchmark(
  unwrapped = {
    x <- numeric(10000)
    for (i in 1:10000) {
      x[i] <- i * (i + 1) / 2
    }
    x
  },
  wrapped = {
    tri_nums <- function(n) {
      x <- numeric(n)
      for (i in 1:n) {
        x[i] <- i * (i + 1) / 2
      }
      x
    }
    tri_nums(10000)
    },
  vapply = vapply(1:10000, \(i) i * (i + 1) / 2, numeric(1)),
  check = 'equal'
)
#> Unit: microseconds
#>       expr      min       lq     mean    median       uq       max neval
#>  unwrapped 2652.487 3006.888 3445.896 3150.7555 3832.094  7029.949   100
#>    wrapped  398.534  414.010  455.333  439.7445  469.307   656.074   100
#>     vapply 4942.000 5154.639 5937.333 5453.2880 5969.760 13730.718   100
Run Code Online (Sandbox Code Playgroud)

创建于 2023-01-04,使用reprex v2.0.2

r2e*_*ans 7

它正在对您的函数进行字节编译。

\n

我们可以通过以下方式确认即时 (JIT) 编译:

\n
compiler::enableJIT(-1)\n# [1] 3                        # <--- this is the previous JIT level\n
Run Code Online (Sandbox Code Playgroud)\n

其中负数返回当前级别不变,值 3 表示最高 JIT 编译级别。我不确定每个级别都在做什么步骤,但我们可以做一个简单的测试来比较它们。(看?enableJIT参考资料 了解更多信息。)

\n
compiler::enableJIT(0)\n# [1] 3\ntri_nums <- function(n) {\n  x <- numeric(n)\n  for (i in 1:n) {\n    x[i] <- i * (i + 1) / 2\n  }\n  x\n}\nbench::mark(\n  unwrapped = {\n    x <- numeric(10000)\n    for (i in 1:10000) {\n      x[i] <- i * (i + 1) / 2\n    }\n    x\n  },\n  JIT0 = tri_nums(10000),\n  vapply = vapply(1:10000, \\(i) i * (i + 1) / 2, numeric(1))\n)\n# # A tibble: 3 \xc3\x97 13\n#   expression      min   median `itr/sec` mem_al\xe2\x80\xa6\xc2\xb9 gc/se\xe2\x80\xa6\xc2\xb2 n_itr  n_gc total\xe2\x80\xa6\xc2\xb3 result memory     time       gc      \n#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:by>   <dbl> <int> <dbl> <bch:t> <list> <list>     <list>     <list>  \n# 1 unwrapped    8.21ms    8.7ms      113.   78.2KB    7.07    48     3   424ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 2 JIT0         7.26ms   7.72ms      128.   78.2KB    9.84    52     4   407ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 3 vapply       5.97ms    6.5ms      152.   78.2KB    9.51    64     4   421ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# # \xe2\x80\xa6 with abbreviated variable names \xc2\xb9\xe2\x80\x8bmem_alloc, \xc2\xb2\xe2\x80\x8b`gc/sec`, \xc2\xb3\xe2\x80\x8btotal_time\n
Run Code Online (Sandbox Code Playgroud)\n

(我不能同时放入所有三个级别,因为我相信 JIT 检查在我们调用它以及定义它时就完成了。我真的没有资格谈论 R 内部的这个级别,所以...请纠正我和/或添加放大信息。)

\n

对级别 1-3 再次执行此操作并复制/粘贴相关bench::mark行,我们看到:

\n
# # A tibble: 3 \xc3\x97 13\n#   expression      min   median `itr/sec` mem_al\xe2\x80\xa6\xc2\xb9 gc/se\xe2\x80\xa6\xc2\xb2 n_itr  n_gc total\xe2\x80\xa6\xc2\xb3 result memory     time       gc      \n#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:by>   <dbl> <int> <dbl> <bch:t> <list> <list>     <list>     <list>  \n# 1 unwrapped    8.21ms    8.7ms      113.   78.2KB    7.07    48     3   424ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 2 JIT0         7.26ms   7.72ms      128.   78.2KB    9.84    52     4   407ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 2 JIT1        419.6\xc2\xb5s  502.5\xc2\xb5s     1923.  108.7KB    0      962     0   500ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 2 JIT2        413.4\xc2\xb5s  494.3\xc2\xb5s     1971.  108.7KB    0      986     0   500ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 2 JIT3        426.7\xc2\xb5s  498.3\xc2\xb5s     1981.  108.7KB    0      991     0   500ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# 3 vapply       5.97ms    6.5ms      152.   78.2KB    9.51    64     4   421ms <dbl>  <Rprofmem> <bench_tm> <tibble>\n# # \xe2\x80\xa6 with abbreviated variable names \xc2\xb9\xe2\x80\x8bmem_alloc, \xc2\xb2\xe2\x80\x8b`gc/sec`, \xc2\xb3\xe2\x80\x8btotal_time\n
Run Code Online (Sandbox Code Playgroud)\n

表明绝大多数收益都在字节编译的第一级(考虑到该函数的简单性,这并不奇怪)。

\n
\n

注意:对于任何实际测试部分代码的人,您可能需要确保回到默认级别 3:

\n
compiler::enableJIT(3)\n
Run Code Online (Sandbox Code Playgroud)\n