我最早写 R 的时候,处理一个两千行乘两百列的基因表达矩阵,想要按行算标准差。当时还不太会向量化操作,老老实实写了个 for 循环,从第一行扫到最后一行。两百行数据还好,等到两千行数据一跑,我自己去倒了杯水,回来还没算完。后来我把循环改成一行apply(mat, 1, sd),几乎秒出结果。就是从那次开始,我意识到“R 中的向量化操作”不是一种优化技巧,而是用好 R 语言的基本功。
向量化操作,简单说就是一次对整个向量、整列或整块数据完成计算,而不是让 R 一个元素一个元素地解释执行。R 是解释型语言,它的循环解释开销非常大;但 R 底层大量核心函数是用 C 和 Fortran 写的,只要你把“批量计算”这件事交给这些内建函数,R 完全可以跑得飞快。这篇文章我会从一个正经的 R 使用者的角度,把向量化的原理、常用函数、真实改造过程、踩坑经验全部讲透,适合刚接触 R 的新手,也适合写了很久循环但始终没有系统理解向量化的中级用户。
1. 向量化操作的本质:R 快不快,关键看循环交给了谁
1.1 为什么 for 循环在 R 里格外慢:解释器才是瓶颈
很多从 Python 或 C 转过来的人会很不适应:R 的 for 循环怎么这么慢?其实问题不在“循环”本身,而在 R 的解释执行机制。R 代码在执行前要先被解析成抽象语法树,然后在运行时一层层做类型检查、函数查找、变量绑定和内存分配。你在 R 里写for (i in 1:100000) { x[i] <- i^2 },解释器就要重复十万次判断i是什么类型、x[i] <-该怎么赋值、往哪个内存地址写数据。每一次都是同样的流程,但 R 不会像 C 编译器那样提前优化,它老老实实把同样的解释动作重复十万遍。
打个比方,for 循环就像一个人做菜时每放一勺盐都要停下来打开菜谱重新读一遍“放盐”的步骤;向量化则像是你先把整套做菜流程背下来,然后一口气把十道菜做完。R 的 for 循环慢,不是因为你的算法不行,而是解释器实在干不了那么多重复的杂活。这就像用一家翻译公司逐字逐句给你翻译一本十万字的书,每一个字都要走一遍“查词、排版、审核”的流程,自然又慢又贵。
1.2 真正向量化的秘密:内建函数背后是 C 语言循环
那为什么sum(x)、mean(x)、cumsum(x)这些函数处理同样的十万个元素时飞快?因为这些核心函数的循环在 C 或 Fortran 里面。你要知道,sum(x)内部其实也是一个循环,它要遍历向量 x 的所有元素累加,但那个循环是编译好的机器码,不需要像 R 解释器那样边解释边执行。R 解释器只做了一次函数调用,然后就把工作丢给了底层编译好的程序。
所以向量化的本质,是“把 R 层的循环转移到 C 层去”。你可以把 R 想成一位项目总监,for 循环就是总监亲自盯着每一个工人干活,每盯一个人都要交代一遍流程;向量化则是总监把整批任务直接交给一条高度自动化的流水线,流水线自己从头跑到尾。写 R 代码追求向量化,本质上是在琢磨“我能不能把这一大批任务塞进某个已经写好的、底层是 C 的流水线”。
下面这个表格很能说明问题:
| 写法 | 实际的执行位置 | 循环执行位置 | 速度感受 |
|---|---|---|---|
for (i in seq_along(x)) x[i] <- x[i] * 2 | R 解释器 | R 层循环 | 慢 |
x * 2 | C 层 | C 层循环 | 极快 |
apply(mat, 1, mean) | 外层 C,内层回调 R 函数 | 近似 R 层 | 中等 |
rowMeans(mat) | C 层 | C 层循环 | 极快 |
要特别提醒一句:不是所有封装好的函数都算向量化。有些函数只是把你明写的 for 循环藏到了黑盒里,内部还是逐元素调用 R 函数,这种函数写起来很方便,但性能没有本质提升。判断标准很简单:如果一个函数可以用在长度为 1 的向量上,也能用在长度为一百万的数据上,并且内部没有逐元素回调 R 层函数,那么它才是真正向量化的。后面讲到 apply 家族时,我会再展开这一点。
2. 高频向量化函数的正确用法与特性
2.1 ifelse 和 case_when:条件判断也要整列处理
R 新手往往会写出这样的代码:
df$type <- NA for (i in 1:nrow(df)) { if (df$score[i] > 60) df$type[i] <- "pass" else df$type[i] <- "fail" }且不说代码冗长,这个循环在数据量大时会明显发飘。换成向量化的ifelse后:
df$type <- ifelse(df$score > 60, "pass", "fail")ifelse会一次性对整个逻辑向量df$score > 60做判断,返回一个与输入等长的向量。这里要注意一个特性:ifelse返回结果的类型取决于“是”和“否”两个分支的类型。如果yes是数值、no是字符,R 会把整个结果强制转成字符,这是很多人踩过的坑。
如果条件嵌套层次一多,ifelse嵌套会让代码变得很难读,而且嵌套层数越深越容易出错。这时我推荐用dplyr::case_when,它同样基于向量化逻辑,但写起来像一张清晰的规则表:
library(dplyr) df$level <- case_when( df$score >= 90 ~ "A", df$score >= 75 ~ "B", df$score >= 60 ~ "C", TRUE ~ "D" )case_when内部的每个条件都是对整个向量计算的,而且它是从左到右逐条匹配,一旦命中就不再继续判断,天然帮我们实现了优先级。用向量化做条件判断,不光是为了速度,更重要的是代码一眼就能看懂,不会被几十行的循环绕晕。
2.2 cumsum、diff、which:让时间序列和筛选逻辑飞起来
cumsum、cumprod、diff这类函数是“比 for 循环更隐蔽的日常需求”的救星。举个例子,你手里有一份每日销售额数据,想算“从年初到当天”的累计销售额,新手可能会写循环,但一行cumsum(sales)就能完成。cumsum内部是 C 层的递推循环,效率极高,而且它返回的向量长度和输入一致,非常适合直接塞进dplyr::mutate:
library(dplyr) daily_sales %>% mutate(cum_sales = cumsum(sales))which的向量化价值主要体现在筛选逻辑上。比如你想知道向量中哪些位置满足某个条件,which(x > 10)会一次性完成逻辑判断和位置查找。配合which.max、which.min还能直接拿到最大值、最小值第一次出现的下标,不用自己写循环跟踪索引。
diff可以快速计算序列的一阶差分。计算股价每日涨跌、种群数量变化率这类任务时,diff(log(x))一行就能代替一整个循环,而且不会因为循环变量赋值错误产生偏差。
# 计算每日收益率 returns <- diff(log(prices))很多时间序列的预处理本质上都是这样一组向量化函数的组合:先diff做差分,再cumsum做累计回推,中间加一个which定位关键拐点。只要把任务拆成“整列处理”的视角,就会发现很多以前写循环的地方其实都有现成的向量化函数。
2.3 向量回收(recycling):好用,但也会悄悄改掉你的数据
R 有一个非常方便但也非常危险的机制叫做“向量回收”。当你对两个长度不同的向量做运算时,R 会自动把短的向量循环补齐到长的向量长度。比如:
x <- 1:10 y <- c(1, 2) x + y你会得到2 4 4 6 6 8 8 10 10 12。R 把c(1, 2)重复了五次,然后与 x 相加。如果短的向量长度恰好是长向量的约数,R 不会给任何警告,直接静默补齐。
这在水准参差的项目代码里堪称“静默杀手”。假设你有一个 1000 行的数据框,其中一个排序后的向量在子集筛选后只剩 2 个值,你手滑把它和另一个 1000 长度的向量相加,得到的结果看似正常,实际上后面 998 个值全部对应错了。为了避免这种坑,我的习惯是在做向量运算前先用length()确认关键向量的长度是否一致;长度不一致时主动用rep_len或rep补齐,而不是指望 R 的回收机制帮你做对事。回收规则本身是向量化里一个省内存的好设计,但只有你清楚自己在做什么时才值得用。
3. 手把手改造一个 for 循环:从模拟抽样到批量分组计算
3.1 案例一:一万次蒙特卡洛模拟的对比实验
还是用实际案例说话。假设我们要模拟一万次实验,每次抽 200 个服从伯努利分布(成功概率 0.5)的样本,然后计算每次抽样的样本均值是否落在理论均值 0.5 的 95% 置信区间内,最后统计覆盖率。未经优化的写法是:
set.seed(42) n_sim <- 10000 n <- 200 coverage <- 0 for (i in 1:n_sim) { x <- rbinom(n, size = 1, prob = 0.5) mu_hat <- mean(x) if (abs(mu_hat - 0.5) < 1.96 * sqrt(0.25 / n)) { coverage <- coverage + 1 } } coverage / n_sim这个循环哪里慢?慢在每次迭代都会调用一次rbinom生成一个 200 长度的向量,然后算mean,再做判断。虽然逻辑本身不复杂,但一万次迭代每次都要走一遍 R 解释器。
向量化的思路是:一次性生成全部10000 * 200个随机数,组成一个 200 行、10000 列的矩阵。矩阵的每一列就是一次模拟实验的全部样本。这样随机数生成只用调用一次,后面的“按列求均值”则用colMeans:
set.seed(42) n_sim <- 10000 n <- 200 samples <- matrix(rbinom(n_sim * n, size = 1, prob = 0.5), nrow = n, ncol = n_sim) sample_means <- colMeans(samples) coverage_ratio <- mean(abs(sample_means - 0.5) < 1.96 * sqrt(0.25 / n)) coverage_ratio循环版本和向量化版本的结果是一样的,但后者只有三行核心逻辑。我在自己机器上实测过类似规模的模拟:for 版本要 5 秒上下,向量化版本基本在 0.1 秒以内,差异能到几十倍。向量化版本的代价是生成一个大矩阵占内存,但200 * 10000个数值只占约 16 MB,完全可接受。如果模拟规模继续增大到每批一万个样本、重复十万次,那才需要考虑分块处理或换并行方案。向量化的精髓在于,把 R 层的一万次循环,压缩成一次大型矩阵运算加几个 C 层函数调用。
3.2 案例二:分组统计与分组建模怎么写得又快又稳
很多时候我们面对的不是单个向量,而是“按组处理”的场景。比如有一个公司员工数据,想计算每个部门的平均薪酬和人数。用 base R 的tapply就很舒服,它本质上把“拆分-计算-合并”三件事一次性完成:
set.seed(1) df <- data.frame( dept = rep(c("A", "B", "C", "D", "E"), each = 10000), salary = rnorm(50000, mean = 8000, sd = 2000) ) tapply(df$salary, df$dept, mean)如果你想得到的是整洁的数据框,用dplyr::group_by+summarise更直观:
library(dplyr) df %>% group_by(dept) %>% summarise(avg_salary = mean(salary), n = n())dplyr在底层也做了很多 C 和 C++ 层面的优化,分组的拆分与合并不会是性能瓶颈。这里想强调的是:当你发现自己要用 for 循环按组跑一遍时,优先考虑“拆分-计算-合并”的思路。split把数据框拆成组列表,lapply遍历计算,最后bind_rows拼回:
result <- df |> split(df$dept) |> lapply(function(d) data.frame(dept = d$dept[1], avg = mean(d$salary))) |> dplyr::bind_rows()这个习惯在处理“按组拟合模型”时特别有用,比如对每个部门拟合一个线性模型,再把系数汇总出来。循环当然可以做,但split + lapply写起来更干净,而且不太容易因为在循环里新增变量时忘记初始化而导致索引错位。严格来说,lapply并不是纯向量化,但它比 for 循环更接近向量化的思维模式,具体差异我放在第 4 部分细说。
3.3 案例三:用 sweep 和 outer 代替行/列方向的逐行循环
矩阵和数据框里的行方向操作最容易让人产生写循环的冲动。比如你想对表达矩阵的每一行减去该行的均值,得到“中心化”后的矩阵。一个直觉写法是 for 循环按行减,但其实可以直接用sweep:
expr_mat <- matrix(rnorm(1000 * 100), nrow = 1000) row_means <- rowMeans(expr_mat) centered <- sweep(expr_mat, 1, row_means, "-")sweep的第一个参数是矩阵,第二个参数1表示沿行方向操作,第三个参数是要减掉的统计量,第四个参数"-"是运算方式。sweep内部会利用向量回收和 C 层循环一次完成整个矩阵的运算。类似的还有scale,它可以同时完成按行/列中心化和标准化,是很多统计建模前处理的标准选择。
outer是另一个能消灭双重循环的函数。假设你想计算一个向量 a 中每个元素与向量 b 中每个元素乘积组成的矩阵,for 循环需要两层嵌套,outer(a, b, "*")一行就解决了。outer(a, b, function(x, y) x^2 + y)还可以自定义计算函数,它会自动把 a 和 b 扩展成矩阵再利用向量化函数计算。这些函数的价值在于,当年你第一反应是“我要写个双层循环”的时候,它们提供了一条完全不同的批量计算路径。我个人的习惯是:遇到双重循环,先停下来想一下,能不能用outer、sweep或者矩阵乘法%*%表达。很多情况下是可以的,而且代码会短到你意想不到。
4. apply 家族是“准向量化”:比 for 清晰,但别神化它
4.1 apply 的底层到底怎么回事
R 里的apply、lapply、sapply、vapply、mapply经常被当成“向量化”的代名词,这种说法其实不够准确。以apply(mat, 1, sd)为例,它的外层确实是用 C 写的循环,效率比 R 层的 for 循环高一些;但关键问题是,apply在处理每一行时都要把这一行提取出来,然后回调 R 函数sd,也就是说内部仍然存在 R 层的函数调用。真正的向量化应该是一整块数据一次性交给 C 层,而不需要逐行回调 R 函数。
所以更精确的说法是:apply 家族是“在 R 层写循环,但通过封装简化代码和减少解释器负担”的一种折中方案。它比手写 for 循环清晰得多,也避免了循环变量泄漏、索引越界等低级错误,但如果你追求极致的性能,很多apply的使用场景可以被更专业的向量化函数替代。比如按行求均值,别用apply(mat, 1, mean),用rowMeans(mat);按列求均值,用colMeans(mat);按行求标准差,matrixStats包里的rowSds比apply(mat, 1, sd)快得多。道理很简单:rowSds内部是纯 C 循环,不逐行回调 R 函数。
4.2 我的选型顺序与实用建议
在实际项目里,我给自己定了一条代码选型顺序。拿到一批数据需要“每行做什么”或“每列做什么”时,我的思考顺序通常是这样:先想有没有现成的矩阵化函数;没有就查一下matrixStats、dplyr、data.table这类包;再不行才用 apply 家族;最后才落到 for 循环。
| 你需要的操作 | 推荐写法 | 理由 |
|---|---|---|
| 按行求均值 | rowMeans(mat) | 纯 C 循环 |
| 按列求均值 | colMeans(mat) | 纯 C 循环 |
| 按行求标准差 | matrixStats::rowSds(mat) | 包内实现 C 循环 |
| 按列求分位数 | apply(mat, 2, quantile) | 没有内建替代,apply 够用 |
| 对列表逐元素训练模型 | lapply(model_list, fit) | 列表遍历,lapply 最清晰 |
| 每行必须执行自定义 R 函数 | apply(mat, 1, my_func) | 无法避免 R 层回调,只能接受 |
vapply也值得单独说。它和sapply的区别在于需要你预先指定返回值的类型和长度,虽然写起来多一个参数,但能确保输出结构稳定。比如vapply(df, is.numeric, logical(1))会返回一个带列名、类型确定的逻辑向量,不会因为数据恰好全是数值而返回矩阵、偶尔有空值就返回列表。用vapply等于提前给函数返回值做了断言,让代码更健壮。
5. 向量化思维在真实 R 数据分析场景里的延伸
5.1 数据清洗和特征工程:绝大多数其实是向量操作
如果你去翻一份正经的 R 数据分析案例,会发现其中大量代码本质上都是向量化操作:读入数据后用log1p对所有正数列做对数变换,等于对好几列同时应用向量化函数;对缺失值做标记时,is.na(df$col)返回逻辑向量,配合which可以定位缺失位置;把连续变量离散化成年龄段,用cut就能一次切分,cut内部也是一个向量化过程,它会生成一个因子向量,而不是循环处理每个元素。
特征工程往往是性能瓶颈重灾区。假设你有用户注册时间戳,要生成“是否工作日”和“小时区间”两个特征,最自然的做法是:
df <- df |> mutate( weekday = weekdays(reg_time), hour = as.integer(format(reg_time, "%H")), is_rush = hour %in% c(8, 9, 18, 19) )weekdays、format、%in%都是向量化函数。它们背后没有隐藏任何你不可控的 R 层循环,处理百万行数据也不在话下。反过来说,如果你对每一行都调一次format,再手动拼成向量,那代码就会又慢又容易出错。新手写 R 数据分析代码慢,往往不是计算量真的大,而是把本来能整列处理的操作拆成了逐行循环。
5.2 生态学中的 α多样性:一行向量化算出香农指数
以热词里提到的“α多样性 R 语言”为例。α多样性指数是基于一个样本内物种丰度分布计算的,最常用的香农指数公式是:
H′ = −∑(p_i × ln p_i)
其中 p_i 是第 i 个物种的相对丰度。如果你有一个 OTU 表,行是样本,列是物种,要计算每个样本的香农指数,很多初学者会写双循环——外层遍历样本,内层遍历物种。正确做法是先做矩阵归一化,再用向量化函数按行操作:
shannon_index <- function(x) { x <- x[x > 0] # 去掉丰度为0的物种,避免 log(0) p <- x / sum(x) -sum(p * log(p)) } otu_table <- as.matrix(otu_df[, -1]) apply(otu_table, 1, shannon_index)这个函数内部全程向量化,只在 apply 层面对每个样本调用一次。如果样本量很大、OTU 表动辄几千行,也可以把整个矩阵的运算写成非 apply 的纯矩阵操作——比如先计算每个元素p * log(p)(利用分子是矩阵、分母是行和,用sweep实现除法),再把所有元素按行求和。这样做,生态学数据最常见的 α 多样性计算也能全程不写显式循环。向量化思维在这里体现为:把“一个样本、一个样本地算”转换成“所有样本的丰度矩阵同时参与算术运算”。
5.3 统计模型与森林图/ARIMA 场景中的向量化影子
另一个热词是“forestploter 做森林图”。森林图的输入数据往往长这样:每一行是一个变量,包含点估计值、置信区间上下限、P 值等。画图之前你要对几十个变量的估计值做整理,比如算HR和CI_low、CI_high的文本标签,最直观的代码是:
plot_data <- plot_data |> mutate( hr_text = sprintf("%.2f", hr), ci_text = sprintf("%.2f - %.2f", ci_low, ci_high) )sprintf本身就是向量化函数,一次处理整列。你可以想象,如果每个变量都手动拼一次字符串,不仅人累,还容易出错。森林图只是输出端,真正的统计模型运算同样是向量化的。ARIMA 模型的拟合和预测,底层依赖矩阵运算和线性代数库,而不是 R 层循环。使用arima()、forecast::auto.arima()时,虽然你在 R 里只调用了一个函数,但模型内部的差分、自相关计算、参数优化都使用了高度优化的向量与矩阵运算。这也是为什么这类模型能在中长序列数据上跑得很快。
我想表达的核心是:向量化不只是“性能优化”,它其实贯穿了 R 数据分析的主线。dplyr的数据操作、forestploter出图前的数据整理、arima背后的矩阵计算、生态学指数的批量计算,到处都有向量化的影子。看懂这一点,你的 R 水平会迈过一个很重要的坎。
6. 写向量化代码容易踩的坑:诊断与排查实录
6.1 静默的回收规则:别让结果错了还不知道
我在第 2.3 部分提过回收规则,这里给一个具体到让人后背发凉的例子。假设有两个变量:
x <- c(10, 20, 30, 40, 50, 60) y <- c(100, 200, 300) x + y因为你给的 y 长度 3 正好是 x 长度 6 的二分之一,R 不会给任何提示,直接把 y 重复两次得到结果。如果你本意是让 x 的第一个元素对应 y 的第一个元素,后面一一对应,那么从第三个元素开始,对应关系就已经错了。
排查这类问题,最笨也最可靠的办法是给关键运算加断言:
stopifnot(length(x) == length(y)) x + y或者在读入外部数据后,用all.equal(length(x), length(y))确认维度。另一个更隐蔽的情况发生在数据框子集操作与向量运算混在一起的时候,比如df$col_a[df$group == "g1"] + df$col_b,前者长度可能远小于后者,但若恰好成倍数关系,整个结果看似正常,实际上语义完全错了。向量化代码写起来快,但千万不要省掉对数据形态的确认,否则一错错一大片。
6.2 因子、日期与 ifelse 的类型“背刺”
类型转换是另一个高频坑。R 中因子变量长得像字符,内部实际是整数编码。直接对因子做as.numeric()会得到内部编码,而不是因子标签对应的值。必须用as.numeric(as.character(f))先转成字符再转数值。这个坑在数据清洗时非常经典:读入 CSV 后一列 ID 被自动识别成因子,你想把 ID 转成数值继续做匹配,直接转永远得到 1、2、3…… 而不是原来的 ID。
ifelse的类型强制也值得留意。当yes和no的类型不一致时,R 会把结果转成更“宽松”的类型。比如你写ifelse(condition, Sys.Date(), as.Date("2020-01-01"))时通常没问题,但如果你在别人代码里看到ifelse(condition, date_col, NA),其中NA是逻辑型,R 会把整个 Date 对象转成数值,导致日期数据显示成一串数字。解决方式是用dplyr::if_else,它会强制要求yes和no类型一致,不一致就直接报错,从根上杜绝类型静默改变。
6.3 向量化后还是慢?用 profvis 找到真正的性能瓶颈
有些时候你已经写了很漂亮的向量化代码,但整体流程还是慢。这时候不要盲猜“是不是这里的 apply 太慢”,要把代码跑一遍性能剖析,看火焰图。profvis是 RStudio 里最顺手的工具:
library(profvis) profvis({ # 你的完整数据分析和建模代码 })跑完后你会看到每一行代码的耗时和内存分配情况。我在实际项目中遇到过很典型的情况:向量化计算只花了 0.2 秒,但之前有一行把几百 MB 的 CSV 用read.csv读进来,花了两分钟。这种问题靠优化循环永远解决不了,只有 profile 才能看到全局。向量化和性能剖析是一对搭档,向量化负责把单步操作的时间压下去,profvis 负责告诉你时间到底消耗在哪一步。
6.4 常见问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| for 循环处理十万行数据慢到无法忍受 | 循环体在 R 解释器逐行解析 | 用向量化函数替代,或先profvis定位热点 |
| 结果算出来和预期差很多但没报错 | 向量回收静默补齐 | 用stopifnot检查长度,关键位置用length()确认 |
| 因子列转数值后得到 1、2、3 | 直接as.numeric把整数编码当数值 | 先as.character再as.numeric |
ifelse结果里日期变成数字 | yes/no类型不一致触发强制转换 | 改用dplyr::if_else |
apply(mat, 1, myfunc)依然很慢 | 自定义函数内部有大量 R 层操作 | 优化 myfunc 本身,或考虑Rcpp |
| Windows 上安装需编译的包报 “Rtools is required to build R packages” | 缺少对应版本的 Rtools | 到 CRAN 官网安装匹配 R 版本的 Rtools,然后重启 R 会话 |
7. 向量化解决不了的时候怎么办
向量化不是万能的。虽然绝大多数“每行都要做不同事情”的场景可以被矩阵化表达,但如果计算本身存在强烈的顺序依赖,比如状态转移、递推预测、递归搜索,向量化就很难直接下手。这种时候我建议按顺序做三件事。
第一,把循环次数降下来。循环不可避免时,把能提出来的向量化部分全部提到循环外面。循环里只保留真正需要“按顺序跑”的逻辑,而不是塞进一堆本来可以整列操作的代码。
第二,考虑用编译好的扩展包。Rcpp可以把 R 函数改写成 C++,对顺序依赖的递归计算尤其有效。比如一个十万步的递推,R 循环可能要好几分钟,而 C++ 只需要一瞬间。这个优化幅度不是百分之几十,而是几个数量级。如果你对性能有硬性要求,学一点 Rcpp 非常值得。Rcpp的代码风格也不会太复杂,常见的递推和累加逻辑跟 C 语言非常接近。
第三,换并行方案。如果你的任务本来可以拆成多个独立小块,只是每一块在 R 里没法向量化,那就用parallel包做多核并行。在 Linux 和 macOS 上mclapply一行就能实现进程级并行,在 Windows 上则用parLapply配合makeCluster。一个常见的两难是任务“太大”没法放进单个向量,但可以拆成批次;并行处理能绕开单进程的瓶颈,把多个核同时利用起来。不过并行会带来通信和内存开销,小任务用了反而更慢,建议至少每个子任务要跑超过半秒或数据量上百 MB 才值得并行。
调优顺序我一般这样走:先保证算法复杂度没问题,再尽量用向量化函数或成熟的矩阵算子;实在有顺序依赖,压缩循环体、把 R 层回调减到最少;还不够快,再上 Rcpp 或并行。向量化是这套流程里性价比最高的一步,因为它最简单直接,大多数情况下已经不缺性能。
我个人在实际操作中的一个体会是,向量化操作真正改善的不只是速度,还有写代码的心情。每当你发现自己要写一个多层的 for 循环时,先停下来想一想:R 是不是已经提供了某个函数,可以直接对这一整块数据完成你想要的操作?如果有,就用它。这个习惯能帮你省掉大量调试时间,也让你最终交付的代码更像一份干净的分析报告,而不是一段只有在作者自己电脑上才能跑通的实验代码。希望这篇关于 R 中向量化操作的经验总结,能让你少走一些我当年走过的弯路。