为什么在R中'outer'比'for'循环慢?

bmp9r5qi  于 2022-12-25  发布在  其他
关注(0)|答案(1)|浏览(139)
u <- rnorm(10000)
v <- rnorm(10000)

# `outer`
system.time(mat1 <- outer(u, v , `<`))
#    user  system elapsed 
#    1.80    1.34    3.32 

# `for` loop
system.time({
  mat2 <- matrix(NA, nrow = length(u), ncol = length(v))
  for(i in seq_along(v)) {
    mat2[, i] <- u < v[i]
  }
})
#    user  system elapsed 
#    0.97    0.02    1.01 

identical(mat1, mat2)
# [1] TRUE
roejwanj

roejwanj1#

分配和销毁内存需要时间

如果使用bench::press()并设置四个选项,您会感觉到内存分配最多的方法花费的时间最长,正如David Arenburg在注解中所建议的那样。
这四个选项是:

  1. outer().
    1.一个for循环。
  2. vapply()(来自sindri_baldur的注解)。
  3. ``<(rep(x), rep(y))(正是outer()在引擎盖下所做的)。
    我喜欢bench,因为它显示了内存使用情况,图中的每个方面都用n*n矩阵显示了这四种方法的速度,以及垃圾收集的级别。

对于100行,vapply比其他方法慢,并且gc(垃圾收集)没有区别。
但是,一旦数据大于这个值,我们就可以看到vapply()执行的垃圾收集要少得多,而且要快得多。
类似地,在最后一个方面(1e4行和列)中,我们可以看到for循环的垃圾收集较少,而且往往比outer()快。

vapply()使用的RAM最少

你可能会怀疑vapply()的垃圾回收更少,因为它留下了更多的垃圾未回收,然而,如果我们看一下总的RAM使用情况,我们可以看到实际上它使用了outer()的三分之一的RAM:

注意:我不知道在创建一个1x1矩阵时,它实际上是如何使用0字节的--但是如果你真的比较两个标量,你可能根本就不使用矩阵。

垃圾收集级别的含义是什么?

请参阅R Internals一章,写屏障和垃圾收集器:
有三个级别的集合。级别0仅收集最年轻的代,级别1收集两个最年轻的代,级别2收集所有代。在20个级别0集合之后,下一个集合位于级别1,在5个级别1集合之后,下一个集合位于级别2。此外,如果级别n集合无法提供20%的可用空间(对于每个节点和向量堆),下一次收集将在第n +1层(R层函数gc()执行第2层收集)。
理解这一点的方法是,如果一个函数创建了更多的临时对象,然后销毁它们,那么它将执行更多的分配,并进行更多的垃圾收集。

运行模拟并生成第一个图的代码

sizes <- c(1, 1e2, 1e3, 1e4)

results <- bench::press(
    size = sizes,
    {
        set.seed(1)
        u <- rnorm(size)
        v <- rnorm(size)

        bench::mark(
            min_iterations = 10,
            check = FALSE,
            outer = {
                mat <- outer(u, v, `<`)
            },
            loop = {
                mat <- matrix(NA, nrow = length(u), ncol = length(v))
                for (i in seq_along(v)) {
                    mat[, i] <- u < v[i]
                }
                mat
            },
            vapply = {
                mat <- vapply(seq_along(v), \(i) u < v[i], logical(length(u)))
            },
            seq = {
                mat <- as.matrix(
                    `<`(
                        rep(u, times = ceiling(length(v) / length(u))),
                        rep(v, rep.int(length(u), length(v)))
                    ),
                    nrow = length(u)
                )
            }
        )
    }
)

ggplot2::autoplot(results) +
    ggplot2::facet_wrap(ggplot2::vars(size),scales="free_x")

第二个地块的代码

library(ggplot2)
p  <- results  |>
    dplyr::mutate(
        expr = attr(expression, "description"),
        size = as.factor(size))  |>
    ggplot() +
        geom_col(aes(
            x = reorder(expr, mem_alloc),
            y = mem_alloc,
            fill = size
        ), color= "black") +
        facet_wrap(vars(size), scales="free_y") +
    labs(
        title = "Total RAM usage", 
        y = "Bytes", 
        x = "Expression"
    )

免责声明:这些是在一台机器上的结果(一台不起眼的相当旧的笔记本电脑)。我没有像你一样得到outer()for循环之间相同大小的差异,所以你的结果可能会不同。

相关问题