为什么 R 中的“outer”循环比“for”循环慢?

Jul*_*ien 5 performance r vectorization

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

Sam*_*amR 8

分配和销毁内存需要时间

如果您使用bench::press()列出的四个选项,您可以感觉到内存分配最多的方法花费的时间最长,正如 David Arenburg 在评论中所建议的那样。

这四个选项是:

  1. outer()
  2. 一个for循环。
  3. vapply()(来自 sindri_baldur 的评论)。
  4. `<`(rep(x), rep(y))(正是outer()在幕后所做的事情)。

我喜欢bench,因为它显示内存使用情况。该图中的每个方面都通过矩阵显示了这四种方法的速度n*n以及垃圾收集的级别。

在此输入图像描述

对于 100 行,比其他方法慢,并且(垃圾收集)vapply没有区别。gc

然而,一旦数据大于此值,我们可以看到vapply()垃圾收集次数减少了很多,而且速度也快了很多。

类似地,在最后一个方面(1e4行和列),我们可以看到for循环的垃圾收集更少,并且往往比outer().

vapply()使用最少的 RAM

您可能怀疑vapply()垃圾收集较少,因为它留下了更多垃圾未收集。但是,如果我们查看 RAM 总使用量,我们可以看到它实际上使用了大约三分之一的 RAM outer()

在此输入图像描述

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

垃圾收集级别是什么意思?

请参阅 R 内部章节,写屏障和垃圾收集器

收藏分为三个级别。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")
Run Code Online (Sandbox Code Playgroud)

第二个图的代码

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

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