本节摘要:Matlab 的性能文化围绕向量化建立——用整块矩阵运算替代逐元素循环。本节讲热点测量、向量化改写、预分配、JIT 编译器对性能格局的改变,以及逻辑索引与
arrayfun等向量化工具的适用边界。
解释执行的逐元素循环里,每个元素都要经历一次"解释—类型检查—调度"的开销,真正的浮点计算只占零头。向量化的思路是让一次调用处理整块数据,把开销摊薄到百万个元素上。这是 Matlab 社区三十年性能文化的核心,也塑造了这门语言的审美:一段好代码读起来应该像数学公式,而不是像流程。
f1 = @() loopVersion(n); timeit(f1) % 多次运行取中位,可靠的计时方式
或跑性能分析器(编辑器"运行并计时"),拿到按行耗时排序的报告。优化的铁律:热点在哪改哪,直觉指认的嫌疑犯经常是无辜的。
% 逐点版本:约 40 秒(n = 1000 的三重循环规模示意) for i = 1:n for j = 1:n Z(i,j) = sqrt(X(i,j)^2 + Y(i,j)^2) + ... sin(X(i,j)) * cos(Y(i,j)); end end % 向量化版本:同等规模约 1 秒 Z = sqrt(X.^2 + Y.^2) + sin(X).*cos(Y);
加速来自两处:逐元素算符(.^、.*)让每次操作处理整个矩阵;sin、cos 对矩阵的调用进入内部的高度优化路径。第一段还有一个隐藏开销——Z 没有预分配,每写一行都在重新分配内存。就算暂时不想向量化,先加一句 Z = zeros(n) 也能数倍提速:
Z = zeros(size(X)); % 预分配:循环版的第一颗螺丝
Z = sqrt(X.^2 + Y.^2); Z(Z > 3) = 3; % 逻辑索引替代 if 嵌套 W = bsxfun(@minus, M, v); % 老写法:自动扩维 W = M - v; % 新写法:隐式扩展(R2016b 起) R = arrayfun(@safeRatio, A, B); % 逐元素调用函数(可读性优先时)
隐式扩展是演化的好样本:向量减矩阵当年必须 bsxfun,后来语言直接吸收了这个模式——用户惯用法沉淀为语言特性,这条路径在 Matlab 历史上反复出现。
2000 年代中后期 JIT(即时编译)进入 Matlab,朴素的数值循环也快了很多,"不向量化就慢百倍"的旧训诫失效了一半。但向化的代码依然值得写:JIT 对纯数值循环效果好,对涉及对象、字符串、动态字段的循环仍然乏力;更重要的是可读性——向量化表达式与数学公式同构,审读成本更低。我的排序是:能向量化就向量化,向量化后可读性反而变差的(深嵌套、依赖上一步结果的递推)保留循环并预分配。递推依赖是典型例子,x(k) = x(k-1)*a 这种后项依赖前项的计算,天生串行,强行 arrayfun 是自找麻烦。
⚠️ 常见坑:把
repmat与循环混用拼接大矩阵,内存翻了几倍再整体拷贝。改用预分配加切片赋值,或者让隐式扩展直接消掉拼接需求。
把本节方法完整走一遍。背景:地球物理课的一次作业,要对一百万个网格点计算埋深校正,原始代码是同学写的双重循环,跑一次一分多钟,参数要扫两百轮,等不起。过程按"测—改—再测"推进,每一步都留下数据:
n = 1000; [X, Y] = meshgrid(linspace(0, 10, n)); % 版本 0:原始代码(循环且未预分配) f0 = @() rawLoop(X, Y); t0 = timeit(f0) % 实测约 1.2e1 秒量级(机器而异) % 版本 1:只加预分配,循环不动 f1 = @() preallocLoop(X, Y); t1 = timeit(f1) % 约降到原来的五分之一 % 版本 2:完全向量化 f2 = @() vecVersion(X, Y); t2 = timeit(f2) % 约降到原来的百分之一 fprintf('预分配提速 %.1f 倍,向量化再提速 %.1f 倍\n', t0/t1, t1/t2)
结果解读:预分配一项就拿到五倍,是性价比最高的一分钟投入;向量化再拿二十倍,两百轮扫描从四十分钟缩到两分钟,作业当晚就能交。变式与边界:若把元素函数换成含分支的复杂校正式(深度大于阈值走一条公式、小于走另一条),向量化写法要用逻辑索引分段计算——两段分别向量化后按掩码拼回,仍然比循环快一个量级;只有当分段逻辑复杂到互相嵌套时,才退回预分配循环。
排错与记录也有讲究。测量时用 timeit 而非 tic/toc 的原因,是后者受首次调用初始化与系统抖动影响,单次读数不可靠;对比前后版本务必在同一机器同一次会话里完成,跨机器的耗时倍数没有可比性。最后把三个版本的耗时写进代码注释存档,下次有人想"优化"这段代码时,先看到历史数据再决定动不动手——性能优化最常见的浪费,就是优化一段本来只占总耗时百分之一的代码。
接到"程序慢"的投诉,按固定顺序走能在十分钟内锁定方向。第一步跑性能分析器看热点分布,确认瓶颈在计算而不是读写文件——很多"慢程序"其实慢在循环里嵌了 save 或表格追加。第二步看热点行有无未预分配的 growing array、有无 repmat 大拼接。第三步检查数据类型,double 与 single 混用会触发隐式转换,表格与元胞上的循环比纯数值矩阵慢一个量级,把热区数据换成纯数值矩阵是常见的一招。三步走完仍不达标的,才轮到并行与 GPU,那是 6.3 的地盘。
补充一个容易被忽略的维度:内存布局。Matlab 按列存储矩阵,逐列访问连续内存、逐行访问则不断跳页,超大矩阵上两者耗时能差数倍。习惯写法是让最内层循环遍历行号、外层遍历列号;向量化天然规避了这个问题的同时,也把"想清楚数据怎么排"的责任藏了起来——理解这层机制,你才能在向量化不便的场合写出同样快的循环代码。
timeit 与性能分析器,不猜热点;bsxfun,惯用法沉淀为语言特性的又一例;