一、性能优化第一原则先找到瓶颈再优化很多人一说性能优化就马上想到少写几层函数 少用virtual 全部改成指针 手写内存池但实际上最重要的一点是不要凭感觉优化。假设一个程序运行需要10秒其中数据库查询7秒 文件读取2秒 真正C计算1秒这时候花很多时间把计算1秒 ↓ 优化到0.8秒整个程序只是从10秒 ↓ 9.8秒收益很小。真正应该优化的是占用时间最多的部分所以性能优化一般应该遵循测量 ↓ 定位瓶颈 ↓ 修改 ↓ 再次测量而不是感觉这里慢 ↓ 直接改常见性能指标包括CPU占用率 执行时间 内存占用 内存申请次数 磁盘I/O 网络I/O 锁等待时间 线程数量Linux 下可以简单使用top查看CPU 内存也可以使用time ./app查看程序运行时间。更复杂的性能分析还会使用perf gprof 火焰图 Profiler所以面试中如果问C程序怎么进行性能优化最好不要一上来就说少new 少拷贝而是先回答首先通过 Profiling 确定真正的性能热点然后再针对 CPU、内存、I/O、锁竞争等具体瓶颈优化最后重新测试验证优化效果。这句话非常重要。二、减少不必要的内存申请和对象拷贝C 中非常常见的性能开销来自动态内存申请 对象拷贝例如for (int i 0; i 1000000; i) { int *p new int(i); // 使用 delete p; }这里会进行大量new delete动态内存操作通常比普通栈变量int value;成本高。因为内存分配器需要寻找合适内存块 维护分配信息 处理多线程同步所以如果对象很小而且生命周期很短没必要频繁new/delete可以优先考虑栈对象 对象复用 内存池这也是为什么高频小对象场景经常会使用Memory Pool例如网络连接对象 消息节点 定时器节点 任务对象如果创建和销毁特别频繁可以考虑提前申请一大块内存 ↓ 自己管理小块 ↓ 减少malloc/free次数另外一个常见问题就是不必要的拷贝例如void process(std::vectorint data) { }调用process(nums);这里整个vector可能发生一次拷贝如果只是读取void process(const std::vectorint data) { }通常更加合适。因为const引用 ↓ 不复制整个vector例如一个 vector 有100万个int如果频繁值传递拷贝100万个元素显然成本很高。所以大对象参数经常考虑const T 如果需要把对象所有权转移出去可以考虑移动语义例如std::vectorint a createData(); std::vectorint b std::move(a);相比完整复制重新申请内存 复制所有元素移动往往只需要转移内部指针所以减少拷贝 合理使用移动语义是 C 性能优化中很重要的一部分。三、STL容器选错了也可能影响性能C 中经常使用vector list map unordered_map deque但是不同容器底层结构不同。选择不合适就可能产生明显性能差异。例如std::vectorint底层是连续内存所以随机访问 O(1) 缓存友好 遍历速度通常较快而std::listint底层是链表结构Node ↓ Node ↓ Node ↓ Node节点可能散落在不同内存位置。因此 CPU 遍历时缓存命中率通常不如 vector。所以不要简单认为list中间插入O(1) ↓ 所以一定比vector快因为实际性能还涉及查找插入位置 动态内存申请 缓存局部性实际开发中如果没有特殊需求vector往往是非常好的默认选择。另外一个典型问题是std::map和std::unordered_mapmap一般基于红黑树查找O(logN)而unordered_map一般基于哈希表平均查找O(1)如果只是大量根据key查值而不要求有序就可以考虑unordered_map但也不能机械地认为unordered_map一定更快因为还要考虑哈希计算 rehash 内存占用 碰撞所以容器选择应该根据访问模式 数据规模 是否需要有序 是否频繁插入删除决定。vector 本身还有一个很常见的性能优化reserve()例如已经知道大概需要100000个元素不要std::vectorint data; for (int i 0; i 100000; i) { data.push_back(i); }可以std::vectorint data; data.reserve(100000); for (int i 0; i 100000; i) { data.push_back(i); }因为如果不提前 reserve容量不足 ↓ 扩容 ↓ 申请新内存 ↓ 搬迁旧元素 ↓ 释放旧内存可能执行多次。而提前reserve(100000)可以减少很多扩容 元素搬迁四、多线程不是越多越快重点看锁竞争和任务粒度很多人优化性能时会想到加线程例如原来1个线程觉得慢就改成8个线程但多线程并不一定让程序变快。因为线程本身也有成本线程创建销毁 上下文切换 锁竞争 Cache失效 线程调度例如std::mutex mutex; void work() { std::lock_guardstd::mutex lock(mutex); // 大量计算 }即使创建10个线程但是所有线程一开始就竞争同一把mutex最后实际Thread1执行 其他9个等待本质上还是串行甚至比单线程更慢因为增加了线程调度 加锁解锁的成本。所以多线程性能优化中要重点关注锁的粒度例如不要拿着锁执行耗时操作如果可以可以这样Data localData; { std::lock_guardstd::mutex lock(mutex); localData sharedData; } // 离开临界区以后再处理 process(localData);也就是锁内 ↓ 只做必须保护的操作 锁外 ↓ 执行耗时计算尽量缩短临界区如果读很多 写很少还可以考虑读写锁让多个 Reader 同时运行。还有一个问题是任务粒度例如一个任务只需要1微秒却每个任务都创建线程 ↓ 执行 ↓ 销毁线程那么线程管理成本可能比真正任务执行还高。所以经常使用线程池提前创建固定数量线程Worker1 Worker2 Worker3 Worker4任务来了放进任务队列线程直接取任务执行。这样减少频繁创建/销毁线程所以多线程性能核心不是线程越多越好而是合理线程数 降低锁竞争 合理任务粒度 减少上下文切换五、缓存、I/O和一些容易忽视的性能问题现代 CPU 的速度远远高于内存访问速度。所以很多时候数据怎么放也会影响性能。例如std::vectorint data;连续存储1 2 3 4 5 6 7 8CPU 读取一个数据时通常会顺便把附近的数据加载到Cache所以连续遍历for (int x : data) { }一般具有比较好的缓存局部性而链表NodeA → NodeB → NodeC节点可能分散0x1000 0x9000 0x3000CPU 每次都可能要去新的内存位置取数据。因此理论复杂度一样并不代表实际速度一样缓存友好性在高性能 C 中非常重要。另外一个经常成为瓶颈的是I/O例如for (int i 0; i 100000; i) { std::cout i std::endl; }这里std::endl不仅换行还会flush缓冲区大量执行可能影响性能。如果不要求每次立即刷新可以std::cout i \n;同样频繁写磁盘也可能很慢。例如每收到一条消息 ↓ 立刻写一次文件可以考虑先放入缓冲区 ↓ 批量写入网络也一样。例如1000个1字节包通常不如合理批量发送高效。所以Buffer 批处理 减少系统调用也是常见优化方向。实际 C 性能优化还经常关注字符串拼接 对象创建 虚函数调用 内存对齐 False Sharing Cache Miss 锁竞争 系统调用但是这些通常应该建立在已经确认这里确实是性能热点的基础上。不要为了可能快一点把代码改得特别复杂。如果面试官问C开发过程中主要注意哪些性能问题可以这样回答我一般从 CPU、内存、I/O 和并发几个方面考虑。首先通过 Profiling 确认真正的性能瓶颈而不是凭经验直接优化。代码层面会注意减少频繁的动态内存申请和大对象拷贝合理使用引用和移动语义根据访问模式选择合适的 STL 容器比如大量连续遍历通常优先考虑 vector并通过 reserve 减少扩容。多线程方面会控制线程数量和锁粒度减少锁竞争和上下文切换。对于 I/O 密集型场景会通过缓冲和批量处理减少系统调用。如果继续问为什么vector往往比list遍历快可以回答vector 使用连续内存数据具有更好的空间局部性对 CPU Cache 更友好list 的节点通常分散在堆上遍历时需要不断跳转指针容易产生更多 Cache Miss。因此实际性能不能只看算法复杂度。如果问new/delete有什么性能问题可以回答动态内存分配需要经过内存分配器管理频繁 malloc/new 和 free/delete 会产生额外开销在多线程场景还可能存在分配器内部竞争。如果存在大量固定大小、生命周期较短的对象可以考虑对象复用或者内存池减少分配次数。如果问多线程是不是线程越多性能越高可以回答不是。线程过多会增加上下文切换和调度成本如果大量线程竞争同一把锁程序甚至可能比单线程更慢。线程数量应该结合 CPU 核数、任务是 CPU 密集还是 I/O 密集以及实际负载来确定。最后可以把 C 性能优化的思路记成先Profiling ↓ 找到热点 ↓ ┌────────┬────────┬────────┬────────┐ ↓ ↓ ↓ ↓ CPU 内存 I/O 多线程 ↓ ↓ ↓ ↓ 算法 少分配 缓冲 少竞争 容器 少拷贝 批处理 小临界区 Cache 移动语义 少系统调用 线程池真正值得关注的是算法复杂度 内存分配 对象拷贝 缓存局部性 锁竞争 I/O次数而性能优化最重要的一条原则仍然是不要猜哪里慢 ↓ 先测量 ↓ 找到真正瓶颈 ↓ 针对瓶颈优化 ↓ 重新测试因为对于 C 开发来说可测量的优化通常比“感觉上更快”的代码更加可靠。0voice · GitHub