【问题标题】:why is purrr::map2 so slow compared to base mapply?为什么 purrr::map2 与基本 mapply 相比这么慢?
【发布时间】:2018-05-30 15:19:49
【问题描述】:

考虑这个简单的基准

list1 <- as.list(rep(1, 50))
list2 <- as.list(rep(1, 50))

microbenchmark::microbenchmark(
+   map2(list1, list2, sum))
Unit: microseconds
                    expr    min       lq     mean   median      uq     max neval
 map2(list1, list2, sum) 375.31 384.2045 481.8708 407.8115 420.641 7923.58   100

microbenchmark::microbenchmark(
+   mapply(sum, X=list1, Y=list2,  SIMPLIFY = FALSE))
Unit: microseconds
                                                expr    min     lq     mean  median      uq    max neval
 mapply(sum, X = list1, Y = list2, SIMPLIFY = FALSE) 46.187 50.634 57.45634 53.3715 59.8715 127.27   100

为什么purrr:map2 比mapply 慢几乎8 倍?我的意思是,我只是将两个列表中的数字并排相加。

问题是我在当前代码中使用map2,所以我想了解这里的开销是什么(以及如何解决它)

谢谢!

【问题讨论】:

  • 调用每个函数的开销可能会有很大差异,而不是数据计算本身。如果你在更大的数据集上运行它,速度比会小得多。例如,如果我使用以下数据 list1 &lt;- as.list(rnorm(1e6)); list2 &lt;- as.list(rnorm(1e6)) 运行您的代码,则中位数为 map:1.34 秒,mapply:1.06 秒。 map 还是比较慢,但是相对差别要小很多。
  • 有趣的是,使用上面的百万值列表和函数function(X,Y) X+Y,map 比 mapply 快一点:1.02 秒对 1.25 秒,当列表有只有 100 个值(148 微秒对 143 微秒)。

标签: r list lapply purrr


【解决方案1】:

正如@eipi10 在 cmets 中指出的,当使用大量数据时,有一些函数调用开销变得不那么重要:

list1 <- as.list(rep(1, 50000))
list2 <- as.list(rep(1, 50000))
microbenchmark(map2(list1, list2, sum), mapply(sum, X=list1, Y=list2,  SIMPLIFY = FALSE))
Unit: milliseconds
                                                expr      min       lq     mean   median       uq      max neval cld
                             map2(list1, list2, sum) 73.84420 78.21917 82.53853 79.48526 81.28048 218.9266   100   b
 mapply(sum, X = list1, Y = list2, SIMPLIFY = FALSE) 51.92849 54.66514 61.34755 56.99206 58.67459 204.2119   100  a 

mapply 使用.Internal 而purr::map2 使用.Call 来访问进行处理的底层C 函数。它们的工作方式存在一些差异,尤其是在参数评估方面,以及 R 搜索底层代码的方式。

.Internal 上的 R 帮助给出了神秘的信息:

.Internal 调用内置于 R 解释器。

只有真正的 R 向导才应该考虑使用这个函数,而且只有 R 开发人员可以添加到内部函数列表中。

但是,R Internals 手册解释说:

在构建时编译成 R 的 C 代码可以在什么地方直接调用 被称为原语或通过 .Internal 接口,这是非常 类似于 .External 接口,但语法不同。更准确地说,R 维护一个 R 函数名称和相应 C 函数的表 调用,按照惯例,它都以“do_”开头并返回一个 SEXP。这 表(文件 src/main/names.c 中的 R_FunTab)还指定了多少 函数的参数是必需的或允许的,无论是否 参数将在调用之前进行评估,以及函数是否 是“内部的”,因为它必须通过 .Internal 访问 接口,或直接访问,在这种情况下,它在 R 中打印为 .原始的。

和

少数原语是特殊的而不是内置的,即 是否使用未评估的参数输入它们。这很明显 语言结构和赋值运算符所必需的,如 以及 && 和 ||有条件地评估他们的第二个 参数,以及 ~、.Internal、调用、表达式、缺失、on.exit、引用 和不评估他们的一些论点的替代品。

.Call 的帮助文件注释:

如果要经常使用这些功能之一,请指定 PACKAGE (将搜索限制在单个 DLL 中)或将 .NAME 作为其中之一传递 本机符号对象。搜索符号可能需要很长时间, 尤其是在加载了许多命名空间时。

这意味着在使用 .Call 时需要花费一些时间在 DLL 中搜索函数。值得注意的是purr::map2在使用.Call时并没有指定包名,这样做可以减少所需的开销。

【讨论】:

    猜你喜欢
    • 2012-05-05
    • 2011-02-11
    • 1970-01-01
    • 2012-05-25
    • 1970-01-01
    • 2020-11-25
    • 2021-09-04
    • 2013-02-22
    • 2015-03-02
    相关资源
    最近更新 更多