0Pricing
R Academy · 课时

用向量化提升速度

用向量化操作替代显式循环,大幅提升运行速度

用向量化提升速度 是 CoddyKit 上的免费 R Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 R Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 R Academy 课程共包含 4 节课。

为什么向量化很重要

R 是一种解释型语言,因此 for 循环的每次迭代都会产生额外开销,包括函数调用分派、边界检查和类型强制转换。向量化操作会将这些工作交给编译后的 C 代码执行,其速度通常会快几个数量级。

向量化是 R 基础包中影响最大的一种优化方式。

循环与 cumsum() 示例

分别使用 for 循环和内置的 cumsum() 计算累计总和,可以清楚地看到两者的差距。cumsum() 会调用 C 层的编译代码,一次遍历即可处理整个向量。

n <- 500000
x <- rnorm(n)

t_loop <- system.time({
  result <- numeric(n)
  result[1] <- x[1]
  for (i in 2:n) result[i] <- result[i-1] + x[i]
})['elapsed']

t_vec <- system.time({
  result2 <- cumsum(x)
})['elapsed']

cat('Loop  :', t_loop, 's
')
cat('cumsum:', t_vec, 's
')

ifelse() 与 for + if

ifelse(condition, yes, no) 是一种向量化条件函数,可以一次性在整个向量上计算条件。它用一次 C 层遍历替代了逐元素执行的 for + if 循环。

n <- 300000
x <- rnorm(n)

t_loop <- system.time({
  result <- numeric(n)
  for (i in seq_len(n)) result[i] <- if (x[i] > 0) x[i] else -x[i]
})['elapsed']

t_vec <- system.time({
  result2 <- ifelse(x > 0, x, -x)
})['elapsed']

cat('for+if :', t_loop, 's
')
cat('ifelse :', t_vec, 's
')

预分配结果向量

当循环无法避免时,请在循环开始前预分配结果向量。在循环中使用 c(result, new_val) 会在每次迭代时复制整个向量,导致总内存操作数达到 O(n^2)。

n <- 20000

t_grow <- system.time({
  result <- c()
  for (i in seq_len(n)) result <- c(result, i^2)
})['elapsed']

t_prealloc <- system.time({
  result2 <- numeric(n)
  for (i in seq_len(n)) result2[i] <- i^2
})['elapsed']

cat('Growing vector:', t_grow, 's
')
cat('Pre-allocated :', t_prealloc, 's
')

选择正确的预分配类型

使用与数据匹配的类型构造函数,避免预分配过程中发生隐式强制转换:

  • numeric(n) — 双精度浮点数
  • integer(n) — 整数
  • character(n) — 空字符串
  • logical(n) — FALSE 值
  • vector('list', n) — 由 NULL 组成的列表
n <- 5
cat('numeric  :', numeric(n), '
')
cat('integer  :', integer(n), '
')
cat('logical  :', logical(n), '
')
cat('character:', character(n), '
')
cat('list len :', length(vector('list', n)), '
')

colSums() 和 rowSums() 与 apply() 的比较

对于矩阵操作,colSums(m)、rowSums(m)、colMeans(m) 和 rowMeans(m) 都是经过高度优化的 C 例程。它们始终比 apply(m, 1, sum) 更快,因为后者会针对每一行分派一次 R 函数 sum。

m <- matrix(rnorm(1000 * 2000), nrow = 1000)

t_apply <- system.time(apply(m, 2, sum))['elapsed']
t_colsums <- system.time(colSums(m))['elapsed']

cat('apply(m,2,sum):', t_apply, 's
')
cat('colSums(m)    :', t_colsums, 's
')

向量化算术始终很快

向量上的基本算术运算——+、-、*、/、^、sqrt()、log() 和 exp()——全部支持向量化。它们会在一次 C 调用中,对整个向量逐元素执行运算。请始终优先使用它们,而不是循环。

x <- 1:1000000

t1 <- system.time(y <- x^2 + 2*x + 1)['elapsed']

t2 <- system.time({
  y2 <- numeric(length(x))
  for (i in seq_along(x)) y2[i] <- x[i]^2 + 2*x[i] + 1
})['elapsed']

cat('Vectorized:', t1, 's
')
cat('Loop      :', t2, 's
')

使用逻辑子集而不是循环

使用逻辑条件筛选向量是向量化操作。与其循环并根据条件追加元素,不如创建逻辑索引后一次性取子集——底层 C 代码只需遍历一次。

x <- rnorm(500000)

t_loop <- system.time({
  pos <- c()
  for (v in x) if (v > 0) pos <- c(pos, v)
})['elapsed']

t_vec <- system.time({
  pos2 <- x[x > 0]
})['elapsed']

cat('Loop filter:', t_loop, 's
')
cat('Logical idx:', t_vec, 's
')

使用 which() 和 tabulate() 处理索引

当您需要获取 TRUE 值的位置时,which(condition) 是向量化且快速的。对于稠密的整数范围,tabulate(bin_vector) 统计整数出现次数的速度比 table() 更快。

x <- sample(1:10, 100000, replace = TRUE)

t_table    <- system.time(table(x))['elapsed']
t_tabulate <- system.time(tabulate(x, nbins = 10))['elapsed']

cat('table()   :', t_table, 's
')
cat('tabulate():', t_tabulate, 's
')

idx <- which(x == 5)
cat('Positions of 5: first 5 =', head(idx, 5), '
')

循环仍然适用的情况

并非所有循环都不好。在以下情况下,使用循环是可以接受的:

  • 每次迭代都依赖上一次的结果(顺序依赖)
  • 迭代次数较少(< 1000)
  • 循环体调用了没有向量化等价实现的复杂函数

在这些情况下,应重点进行预分配,并避免在循环内部不断扩展数据结构。

# Sequential dependency -- loop is correct here
fib <- function(n) {
  result <- integer(n)
  result[1] <- 1L
  if (n >= 2) result[2] <- 1L
  for (i in seq_len(n)[-c(1,2)]) result[i] <- result[i-1] + result[i-2]
  result
}
cat('Fibonacci:', fib(10), '
')

向量化总结

编写快速 R 代码时的向量化关键规则:

  • 使用 cumsum/cumprod/diff 进行顺序累积
  • 使用 ifelse() 处理逐元素条件
  • 使用 numeric(n) / vector('list',n) 进行预分配
  • 对于 apply(),优先使用 colSums/rowSums/colMeans/rowMeans
  • 逻辑子集优于筛选循环

快速检查:预分配

为什么在循环中使用 result <- c(result, new_val) 扩展向量时,对于较大的 n 会非常慢?

向量化回顾

向量化是 R 提升性能的主要手段:

  • 向量化函数(cumsum、ifelse、算术运算符)会调用已编译的 C 代码,速度比功能等价的 R 循环快 10 到 100 倍
  • 在任何不可避免的循环开始前预分配结果容器,以避免 O(n^2) 的复制
  • 对于矩阵聚合,colSums/rowSums 的速度优于 apply()
  • 逻辑子集能够简洁快速地替代筛选循环

常见问题解答

「用向量化提升速度」课时是免费的吗?

是的 — 「用向量化提升速度」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 R Academy 课程的其余内容,请升级到 CoddyKit PRO。 R Academy 课程共包含 4 节课。

「用向量化提升速度」这节课中我会学到什么?

用向量化操作替代显式循环,大幅提升运行速度 你通过在浏览器中直接运行的动手代码来练习 R Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 R Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 R Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「用向量化提升速度」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 R Academy 课中编写并运行代码吗?

能。每节 R Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. system.time() 与 proc.time()
  2. 使用 Rprof 和 profvis 分析代码性能
  3. 用向量化提升速度
  4. 使用 microbenchmark 进行基准测试
← 返回 R Academy