刚接手一个轨迹平滑项目时,我第一反应是把离散路径交给三次 B 样条去处理,这样最后得到的速度和加速度曲线都比较干净。Matlab 里跑起来之后,最初我以为瓶颈在后面的约束检查上,毕竟要算逐点速度、曲率、避障距离。可是用 profile 看完才发现,整个程序耗时的重灾区居然是“对三次 B 样条逐点求值”这一步。一千个采样点,每个点循环调用一次递归基函数,外层再包一层优化迭代,算一次目标函数慢得像是死循环。
后来我把这部分的写法彻底换掉,核心思路不是去优化某一个基函数子程序,而是把 B 样条求值从“点对点的函数调用”改成“一次线性代数运算”。这个改动让整个 Matlab 程序在优化循环里提速非常明显,尤其是控制点数量和采样点数都过百之后,差别可以达到一个数量级以上。这也是我想分享的主要经验:三次 B 样条本身并不慢,慢是因为你在循环里反复建结构、反复调函数、反复做没有必要的重复计算。
1. 越标准的三次 B 样条写法,往往越慢
很多 Matlab 例程里的 B 样条基函数都是抄的教科书版本,逻辑没有错,但运行效率比较糟糕。这套经典递归公式,也就是 Cox-de Boor 递推,几乎每个搞过样条的人都在论文附录里见过:
- p 次基函数由 p 次递归得到;
- 每个参数点 u 最终只对少数 p+1 个控制点有贡献;
- 但如果按“所有基函数都要算一遍”的思路实现,实际计算量就会膨胀。
问题不在公式本身,而在你把这些公式当成标量函数调用。每个采样点调用一次,每次从零开始递归,中间还有大量数组切片和条件判断。我在当时程序里就是直接在 for 循环里写 N = coxdeBoor(knots, 3, u),采样点有一千个,控制点有十几个,这么算下来除了慢,还特别难扩展。
1.1 只有“正确”的递归写法,没有“高效”的递归写法
Cox-de Boor 递归的经典形式是:
code复制function Nip = baseFun(knots, p, i, u)
if p == 0
if u >= knots(i) && u < knots(i+1)
Nip = 1;
else
Nip = 0;
end
return;
end
然后一路递归到 p=0。当你对每个 i、每个采样点、每次迭代都这么调,Matlab 的函数调用开销、递归栈开销和标量比较都会变成真正的负担。更严重的是,这种写法让循环里很难利用 Matlab 的自动向量化优化。Matlab 底层矩阵运算很快,但脚本级 for 循环加上大量判断语句,会反复进入解释路径,即使循环次数不多也能感到明显卡顿。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1.2 把 B 样条展开成“一条公式”是另一个极端
还有一类程序,为了绕开循环,会把三次 B 样条在某组节点下展开成关于参数 u 的分段多项式。这个办法在做小型离线计算时还好,但只要你把节点改成非均匀分布,或者为了拟合复杂路径增加控制点数量,那条“公式”的长度会立刻变得没法看。更麻烦的是,如果你用了符号工具箱去推导这个表达式,后续每次代入数值都会产生巨大的中间字符串,跑起来反而更慢。
我在实际中遇到的场景是:控制点本身就是优化变量。这意味着我不可能在优化前提前把 B 样条展开成静态的分段多项式,因为每一轮迭代控制点都在变化。可仔细想一下就会发现,节点向量、采样点参数、以及每个采样点对应的基函数索引,在优化过程中是不变的。真正变化的,只有控制点那一组数值。
这个观察是整体提速的突破口。
1.3 先别急着调优化器,让 profile 告诉你卡在哪
那次调试我做过一个比较典型的错误判断:看到目标函数里有避障约束,就去检查是不是每轮迭代都要算上千次距离。结果在一大堆绘图和数据保存代码里,真正最耗时的是 B 样条求值。所以建议任何优化程序在动手重写之前,一定要先用 profile on 和 profile viewer 跑一遍。不要凭感觉猜瓶颈,尤其不要怀疑“这么小的 B 样条能有多慢”。当递归发生在最内层,而它又被优化迭代调用几百次时,慢就是必然的。
提示:用
profile时,给一次完整的单目标函数评估计时即可,别看整个主脚本。如果程序太大,可以先把绘图关闭。profile 结果出来后,把占用时间前三的函数列表截图保存,再开始重构。
2. 真正的加速核心:把基函数抽成固定矩阵
B 样条曲线的定义是:
code复制C(u) = Σ N(i,p)(u) * P(i)
其中 N(i,p)(u) 是第
