3. 代码级优化:算法与数据结构选择、减少不必要的计算、循环优化技巧、避免过度设计
聊到代码级优化,很多人第一反应是「把循环展开」、「用位运算代替除法」。嗯,这些技巧确实有用,但我想先说一句大实话:选错了算法和数据结构,后面再怎么微优化都是白搭。这就好比你要从北京到上海,选了骑自行车,然后拼命给自行车装涡轮增压——方向错了,努力白费。
这一章,我把自己这些年踩过的坑、总结的经验,掰开揉碎了讲给你听。咱们从四个维度来聊:算法与数据结构的选择、减少不必要的计算、循环优化技巧、以及避免过度设计。
3.1 算法与数据结构选择:选对了,事半功倍
我个人习惯,拿到一个性能问题,先不看代码细节,先问自己三个问题:
- 数据量有多大?
- 操作类型是什么?(读多写少?写多读少?还是随机访问?)
- 有没有现成的、经过验证的方案?
举个例子。我在项目中遇到过这样一个场景:一个实时推荐系统,需要频繁地判断某个用户 ID 是否在黑名单里。最初的实现用的是 std::vector,每次查找都是 O(n) 的线性扫描。数据量小的时候还好,用户量一上来,延迟直接飙到几百毫秒。
我当时看了一眼,二话不说改成了 std::unordered_set,查找复杂度降到了 O(1)。改动就一行代码,性能提升了两个数量级。你想想看,这就是数据结构选择的威力。
下面这张图,是我自己整理的数据结构选择决策流程,你可以参考一下:
再说算法。我记得有一次优化一个排序模块,数据量大概 10 万条,原来的实现用了冒泡排序。嗯,你没看错,冒泡排序。我当时差点没忍住笑出来。换成快速排序后,执行时间从 8 秒降到了 0.02 秒。这已经不是优化了,这是「降维打击」。
3.2 减少不必要的计算:少做,就是快
这个原则听起来简单,但实际做起来,很多人会忽略。我见过太多代码,里面充斥着「重复计算」和「提前计算」。说白了,就是做了很多没必要的事。
来看一个典型的例子:
// 不好的写法
for (int i = 0; i < vec.size(); i++) {
if (vec[i] > 100) {
process(vec[i], sqrt(vec[i])); // 每次循环都算 sqrt
}
}
// 优化后的写法
double threshold = 100.0;
for (int i = 0; i < vec.size(); i++) {
double val = vec[i];
if (val > threshold) {
double sqrt_val = sqrt(val); // 只算一次
process(val, sqrt_val);
}
}
你看,改动不大,但效果很明显。把循环不变的计算提到外面,把重复计算的结果缓存起来,这些都是基本功。
我在项目中遇到过更夸张的:一个图像处理算法,每次循环都重新计算一个查找表。那个查找表其实只跟图像分辨率有关,而分辨率在整个处理过程中是不变的。我把查找表的计算提到循环外面,性能提升了 40%。
还有一个常见的优化点:延迟计算。说白了,就是「能不算就不算,实在要算再算」。比如一个对象的某个属性,可能在整个生命周期里都不会被访问到,那就没必要在构造时就计算好。等真正需要的时候再算,这叫「懒加载」。
3.3 循环优化技巧:小改动,大收益
循环是性能优化的「主战场」。程序 90% 的时间都花在 10% 的代码上,而这 10% 的代码里,循环占了绝大多数。
我总结了几个实用的循环优化技巧:
- 减少循环内的函数调用——函数调用有开销,尤其是虚函数。能内联的就内联,能提到外面的就提到外面。
- 使用局部变量——把循环中频繁访问的成员变量或全局变量,先赋值给局部变量。局部变量在栈上,访问速度更快。
- 循环展开——手动或让编译器帮你展开循环,减少循环控制的开销。不过别过度,展开太多会导致指令缓存溢出。
- 消除循环依赖——如果循环的每次迭代都依赖上一次的结果,那就很难并行化。想办法打破这种依赖。
来看一个具体的例子:
// 优化前
for (int i = 0; i < n; i++) {
data[i] = data[i] * factor + offset;
}
// 优化后(循环展开 + 局部变量)
int local_factor = factor;
int local_offset = offset;
int i = 0;
for (; i + 3 < n; i += 4) {
data[i] = data[i] * local_factor + local_offset;
data[i+1] = data[i+1] * local_factor + local_offset;
data[i+2] = data[i+2] * local_factor + local_offset;
data[i+3] = data[i+3] * local_factor + local_offset;
}
for (; i < n; i++) {
data[i] = data[i] * local_factor + local_offset;
}
我曾经优化过一个视频编码模块,里面有个三层嵌套循环。最内层循环每次都要访问一个全局配置表。我把配置表指针提到最外层,用局部变量传递进去,性能提升了 15%。
3.4 避免过度设计:简单,就是最好的优化
这一点,我想多说几句。很多开发者,包括年轻时的我,都有一种「设计癖」——总觉得代码写得越复杂越高级,恨不得把所有设计模式都用上。结果呢?代码是「优雅」了,性能却惨不忍睹。
我记得有一次 review 一个同事的代码,他用了一个抽象工厂模式来创建不同的日志处理器。整个调用链有 5 层虚函数,每次写日志都要经过 5 次间接跳转。我问他:「你这里真的需要多态吗?是不是一个简单的 if-else 就够了?」他想了想,改成 if-else 后,日志写入性能提升了 3 倍。
过度设计带来的性能问题,主要体现在:
- 虚函数开销——每次调用都要查虚函数表,无法内联
- 对象创建销毁——过多的对象创建和销毁,增加 GC 压力或内存碎片
- 间接跳转——函数指针、回调、消息传递,都会破坏 CPU 的分支预测
- 不必要的抽象——为了「可扩展性」而引入的抽象层,往往永远用不到
我曾经接手过一个项目,代码写得「非常优雅」——用了 7 种设计模式,类图画了满满一黑板。但性能测试一跑,惨不忍睹。我花了三天时间,把核心路径上的所有抽象层全部去掉,用最直接的方式重写。性能提升了 10 倍,代码量减少了 60%。
你想想看,用户在乎的是你的代码用了什么设计模式吗?不,用户只在乎你的程序跑得快不快、稳不稳定。
小结
这一章我们聊了四个方面的优化:
| 优化方向 | 核心思想 | 常见手段 |
|---|---|---|
| 算法与数据结构 | 选对工具 | 哈希表、平衡树、快速排序等 |
| 减少计算 | 少做无用功 | 循环不变式外提、延迟计算、缓存 |
| 循环优化 | 压榨循环效率 | 展开、局部变量、消除依赖 |
| 避免过度设计 | 保持简单 | 减少抽象层、避免不必要的多态 |
最后送大家一句话:最好的优化,是那些你不需要做的优化。选对了方向,写对了代码,性能自然就上去了。下一章,咱们聊聊内存优化——这可是个「深水区」,准备好了吗?