从菜鸟到高手:Linux C/C++程序性能分析实战指南!

"你这程序怎么这么卡啊?能不能优化一下?"
—— 你的leader,大概率

大家好,我是小康。

你有没有这样的经历:辛辛苦苦写完的 C++ 程序,功能测试一切正常,但一到生产环境就被吐槽"太慢了"?作为开发者,我们经常被要求解决性能问题,但如何找出程序的性能瓶颈,却是很多人的盲区。

今天,我就用大白话带你入门 Linux 环境下 C/C++ 程序的性能分析(带实战案例),让你面对性能问题时不再抓瞎。不需要高深的理论,不需要复杂的工具,这篇文章读完,你就能实战了!

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

为什么程序会慢?

在深入工具和方法之前,我们先来聊聊为什么程序会慢。一个程序主要在三个方面消耗资源:

  1. CPU时间 - 计算太多、算法效率低
  2. 内存使用 - 内存泄漏、频繁申请释放内存
  3. I/O操作 - 文件读写、网络通信太频繁

今天我们主要聚焦CPU性能分析,因为这通常是最直接影响程序速度的因素。内存和 I/O 问题咱们后面再专门讲。

谁是 CPU 时间的大户?用 top 找出来

既然要分析性能,那首先得知道是不是我们的程序真的耗 CPU。最直观的方法就是用top命令实时监控程序的 CPU 和内存使用情况:

$ top -p $(pgrep 进程名)

这样你就能看到程序的 CPU 使用率。如果一个程序占用 CPU 接近 100%,那它八成是有性能问题了。而且通过 top,你还能看到程序使用了多少内存等信息,这些都是判断程序健康状况的重要指标。

入门级工具:time命令

发现程序确实吃 CPU 后,我们需要更具体地知道它到底慢在哪里。这时可以用 Linux 自带的 time 命令来分析程序的运行时间构成:

$ time ./my_program

执行后你会看到类似这样的输出:

real    0m1.234s
user    0m1.000s
sys     0m0.234s
  • real:实际经过的时间(墙上时钟时间)
  • user:CPU在用户态的执行时间
  • sys:CPU在内核态的执行时间

如果user时间特别长,说明你的程序计算量太大;如果sys时间特别长,说明你的程序系统调用太多。

打个比方,这就像你去餐厅吃饭:

  • real时间是从你进门到出门的总时间
  • user时间是你实际吃饭的时间
  • sys时间是服务员端菜、收拾桌子的时间

性能分析的秘密武器:perf

timetop只能告诉你程序慢,但具体慢在哪个函数,还得靠专业工具。Linux下最强大的性能分析工具之一就是perf

安装perf

# Ubuntu/Debian
$ sudo apt-get install linux-tools-common linux-tools-generic# CentOS/RHEL
$ sudo yum install perf

实战:找出CPU杀手

程序慢了,我们需要找出具体是哪段代码拖了后腿。perf 就是最好的侦探工具:

# 开发环境:从启动开始记录
$ sudo perf record -g ./slow_program# 生产环境:对运行中程序采样30秒
$ sudo perf record -p <进程ID> -g -F 99 sleep 30# 分析结果
$ perf report

开发环境用第一种方式,能看到程序从启动到结束的全过程;生产环境用第二种方式,不用重启服务就能采样数据。perf report会显示哪些函数最耗 CPU,直接指出问题所在!

我曾经遇到过一个实际案例:程序处理大量数据非常慢,用 perf 一看,发现 80% 的 CPU 时间都花在了一个字符串处理函数上。把这个函数优化后,整个程序速度提升了 5 倍。

更直观的火焰图:FlameGraph

perf 的输出有时候不够直观,这时候就需要"火焰图"(FlameGraph)出场了。火焰图能把 perf 的结果可视化,一眼就能看出哪个函数最耗时。

生成火焰图

# 先记录perf数据
$ sudo perf record -p <进程ID> -g -F 99 sleep 30# 导出数据
$ perf script > perf.out# 用FlameGraph工具生成SVG图
$ git clone https://github.com/brendangregg/FlameGraph.git
$ cd FlameGraph
$ ./stackcollapse-perf.pl ../perf.out > ../perf.folded
$ ./flamegraph.pl ../perf.folded > ../flamegraph.svg# 使用 firefox 打开
$ firefox flamegraph.svg

然后用浏览器打开生成的 svg 文件,你会看到一个炫酷的火焰图!图中宽度越大的函数,占用的 CPU 时间就越多。

实战案例:优化一个日志解析程序

前几天我有个小需求,需要解析一些服务器日志文件,提取出所有 ERROR 级别的日志,并生成个简单报告。我写了个第一版的程序,但在处理一个 893MB 的日志文件时,跑了整整 3 分钟才出结果,这也太慢了吧!

代码是这样的:

// slow_parser.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <regex>
#include <vector>struct LogEntry {std::string timestamp;std::string level;std::string message;
};std::vector<LogEntry> parse_log(const std::string& filename) {std::vector<LogEntry> entries;std::ifstream file(filename);std::string line;// 使用正则表达式解析日志格式:[时间戳] [日志级别] 消息内容std::regex log_pattern(R"(\[(.*?)\]\s*\[(.*?)\]\s*(.*))");while (std::getline(file, line)) {std::smatch matches;if (std::regex_search(line, matches, log_pattern)) {LogEntry entry;entry.timestamp = matches[1];entry.level = matches[2];entry.message = matches[3];// 只保留ERROR级别的日志if (entry.level == "ERROR") {entries.push_back(entry);}}}return entries;
}int main(int argc, char* argv[]) {if (argc != 2) {std::cerr << "用法: " << argv[0] << " <日志文件路径>" << std::endl;return 1;}std::cout << "开始解析日志文件: " << argv[1] << std::endl;auto entries = parse_log(argv[1]);std::cout << "共发现 " << entries.size() << " 条ERROR级别日志" << std::endl;// 输出前10条错误日志int count = 0;for (const auto& entry : entries) {if (count++ < 10) {std::cout << entry.timestamp << ": " << entry.message << std::endl;} else {break;}}return 0;
}

编译并测试了下运行时间:

$ g++ -g slow_parser.cpp -o slow_parser
$ time ./slow_parser server.log

运行结果:

real	3m0.753s
user	2m54.315s
sys	0m6.399s

差不多 3 分钟,太离谱了!我决定用 perf 来分析一下到底是哪里慢:

$ perf record -g ./slow_parser server.log
$ perf report

perf report 的结果让我眼前一亮:

Samples: 197K of event 'cycles', Event count (approx.): 94623200788Children      Self  Command  Shared Object        Symbol
+   77.46%    15.58%  a.out    a.out                [.] std::__detail::_Executor<__gnu_cxx::__normal_iterator<char const*, std::__cxx11::basic_string<char, s◆
+   76.84%     5.75%  a.out    a.out                [.] std::__detail::_Executor<__gnu_cxx::__normal_iterator<char const*, std::__cxx11::basic_string<char, s
+   75.84%     5.91%  a.out    a.out                [.] std::__detail::_Executor<__gnu_cxx::__normal_iterator<char const*, std::__cxx11::basic_string<char, s
+   75.01%     4.26%  a.out    a.out                [.] std::__detail::_Executor<__gnu_cxx::__normal_iterator<char const*, std::__cxx11::basic_string<char, s
+   71.60%     0.62%  a.out    a.out                [.] std::__detail::_Executor<__gnu_cxx::__normal_iterator<char const*, std::__cxx11::basic_string<char, s
...
+   48.18%     0.05%  a.out    a.out                [.] std::regex_search<__gnu_cxx::__normal_iterator<char const*, std::__cxx11::basic_string<char, std::cha
...

这里需要理解两个关键列:

  • Self:函数自身消耗的CPU时间百分比
  • Children:函数及其调用的所有子函数消耗的CPU时间百分比

简单说,Self 告诉你"这个函数本身"有多慢,Children 告诉你"这个函数及它调用的所有函数"一共有多慢。性能优化时,通常先看 Children 高的函数找到热点调用链,再看 Self 高的函数找到真正耗时的代码。

虽然输出结果有点复杂,但很明显,大部分 CPU 时间都花在了 std::__detail::_Executorstd::regex_search 这些函数上,这些都是正则表达式相关的函数!看来正则表达式是罪魁祸首。

其实想想也对,正则表达式虽然功能强大,但在处理大量文本时,性能确实不太理想。于是我决定用普通的字符串处理函数来替代正则表达式:

// fast_parser.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <chrono>struct LogEntry {std::string timestamp;std::string level;std::string message;
};std::vector<LogEntry> parse_log(const std::string& filename) {std::vector<LogEntry> entries;std::ifstream file(filename);std::string line;// 预分配空间,减少内存重新分配entries.reserve(10000);// 使用字符串搜索和截取替代正则表达式while (std::getline(file, line)) {size_t first_bracket = line.find('[');size_t second_bracket = line.find(']', first_bracket);size_t third_bracket = line.find('[', second_bracket);size_t fourth_bracket = line.find(']', third_bracket);if (first_bracket != std::string::npos && second_bracket != std::string::npos &&third_bracket != std::string::npos && fourth_bracket != std::string::npos) {LogEntry entry;entry.timestamp = line.substr(first_bracket + 1, second_bracket - first_bracket - 1);entry.level = line.substr(third_bracket + 1, fourth_bracket - third_bracket - 1);entry.message = line.substr(fourth_bracket + 1);// 去除消息前面的空格size_t message_start = entry.message.find_first_not_of(' ');if (message_start != std::string::npos) {entry.message = entry.message.substr(message_start);}// 只保留ERROR级别的日志if (entry.level == "ERROR") {entries.push_back(entry);}}}return entries;
}int main(int argc, char* argv[]) {if (argc != 2) {std::cerr << "用法: " << argv[0] << " <日志文件路径>" << std::endl;return 1;}auto start_time = std::chrono::high_resolution_clock::now();std::cout << "开始解析日志文件: " << argv[1] << std::endl;auto entries = parse_log(argv[1]);std::cout << "共发现 " << entries.size() << " 条ERROR级别日志" << std::endl;// 输出前10条错误日志int count = 0;for (const auto& entry : entries) {if (count++ < 10) {std::cout << entry.timestamp << ": " << entry.message << std::endl;} else {break;}}auto end_time = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);std::cout << "处理耗时: " << duration.count() / 1000.0 << " 秒" << std::endl;return 0;
}

再次编译运行:

$ g++ -O2 fast_parser.cpp -o fast_parser
$ time ./fast_parser server.log

优化后的结果:

real	0m8.188s
user	0m7.240s
sys	0m0.945s

哇!只用了 8 秒多!相比原来的 3 分钟,这简直就是天壤之别啊,速度提升了 20 多倍!

主要优化点:

  1. 使用基本的字符串操作替代了正则表达式
  2. 预分配了 vector 的空间,减少内存重新分配
  3. 增加了 -O2 编译优化选项
  4. 添加了时间测量代码,方便对比性能

这个小实验给我的启示是:虽然正则表达式写起来很方便,但在处理大量数据时,可能成为严重的性能瓶颈。

用性能分析工具找出这些瓶颈,然后用更高效的方法替代,就能大幅提升程序性能。这在实际工作中可是能省下不少时间的技能啊!

性能分析的实用技巧

1、 先用简单工具:不要一上来就用复杂工具。先用 time、top 这些简单命令,确定问题大致在哪。

2、二八原则:程序 80% 的时间往往花在 20% 的代码上。找到这 20% 的"热点"代码是关键。

3、 二分查找法找性能问题:如果项目很大,不知道从哪下手,可以试试"二分法":

  • 把程序的功能模块分成两半
  • 暂时禁用一半,看问题是否还存在
  • 根据结果,继续对有问题的那一半再分成两半
  • 如此反复,直到定位到具体模块

4、编译优化:别忘了编译时的优化选项,比如:

$ g++ -O2 your_program.cpp -o your_program

5、使用性能分析器:除了 perf,还有很多好用的工具,比如 Valgrind 的Callgrind、gperftools等。

6、不要过早优化:先让程序正确运行,再考虑性能优化。过早优化是万恶之源!

总结:性能分析的"三板斧"

如果你是初学者,记住这个简单的流程就够了:

  1. 用 top 监控 CPU 使用率
  2. 用 time 测量总执行时间
  3. 用 perf 找出具体的热点函数

掌握了这"三板斧",基本上就能应对 80% 的性能问题了。至于内存和 I/O 方面的性能分析,我们之后再详细讲解。

记住,性能优化是一门实战性很强的技术,多练习,多分析,你很快就能成为性能调优高手!


如果觉得这篇文章有帮到你,记得 收藏、点赞、关注 哦~ 或者转发给身边的开发者小伙伴!每一次互动都是对我最大的鼓励,也会让更多需要的人看到这篇文章!

下篇文章我们将详细介绍 Linux下C/C++ 程序的内存和 I/O 性能分析方法,敬请期待!

想学习更多 C/C++ 性能优化和 Linux 后端开发知识?欢迎关注我的公众号「跟着小康学编程」,持续分享硬核技术干货和面试秘籍!

怎么关注我的公众号?

扫下方公众号二维码即可关注。

另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/908510.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

US104N-ASEMI家用电器专用US104N

US104N-ASEMI家用电器专用US104N编辑:LL US104N-ASEMI家用电器专用US104N 型号:US104N 品牌:ASEMI 封装:TO-220F 栅极触发电压(Vgt):1.3V。 栅极触发电流(Igt):0.2mA。 保持电流最大值(Ih):30mA。 正向重复电压(Vdrm):800V。 通态电流最大值(It(RMS)):4A。 …

【Nmap】扫描结果美化后的服务器端口安全管理

所有的服务器使用Nmap扫描一下服务器列表的txt就可以,Nmap扫描结果通常是个XML文件,不方便浏览,推荐几个脚本,做一个扫描结果优化 https://github.com/ssjt21/parser_nmap_xml_2_Excel https://github.com/sp4rkw/NmapToExcel https://github.com/chuanwei/nmaptocsvNMAP扫…

plink软件中计算距离矩阵

001、--distance选项,计算具有不同位点的数目plink --file test --distance --out result ## 结果文件为每一个个体具有不同的SNP的数目,下三角矩阵形式展示,省略了第一行和最后一列 。002、

STM32 + keil5 HelloWrold

硬件清单 1. STM32F407VET6 2. STLINK V2下载器(带线)环境配置 1. 安装ST-LINK2. 安装并配置 keil5 https://blog.csdn.net/qq_36535414/article/details/108947292 https://blog.csdn.net/weixin_43732386/article/details/1173752663. 接线并下载烧入完成后拔插ST-LINK V2,就…

MySQL 锁机制:数据库的交通管制系统

MySQL 锁机制:数据库的"交通管制系统" 🚦 在数据的高速公路上,没有红绿灯会怎样?一片混乱!MySQL 的锁机制就是数据库世界的交通规则... 什么是锁机制?🤔 锁机制是数据库用来控制并发访问的一种方式,确保在多人同时操作数据库时不会出现数据不一致或损坏。简…

揭秘AI自动化框架Browser-use(四):Browser-use记忆模块技术解析

在 AI 自动化任务中,记忆模块是实现复杂任务处理的关键组件。Browser-use 项目通过引入记忆模块,解决了 LLM 在连续性任务中的无状态性问题,使代理能够维持上下文连贯性,执行复杂多步骤任务,并从错误中学习和恢复。一、从一次失败的景点采集说起 在 AI 自动化任务中,记忆…

微软Dynamics 365Power Platform技术找工作|宝藏岗位+金饭碗+高薪潜力

【简单介绍】‌ (长沙爱码士IT培训是一家专门培训微软Dynamics 365 CRM和Power Platform技术的一家公司www.aimashi365.com)本人是长沙爱码士IT的金牌讲师,已经帮助上百位学员找到IT工作,对训微软Dynamics 365 CRM和Power Platform技术有兴趣的朋友,我们可以随时沟通交流。…

差分约束学习笔记

一.差分约束系统 如果一个系统有 \(n\) 个变量 \(x_1,x_2,,x_n\) 和 \(m\) 个约束条件(也是不等式)和\(m\) 个常量 \(w_1,w_2,,w_m\)。每一个不等式形如以下格式 \(x_i - x_j \le w_k\)(\(1 \le i,j \le n\),\(1 \le k \le m\))。则称之为差分约束系统。 这个名字的由来是…

9.6K+ Star!一个基于 SpringBoot + Vue3 的工作流引擎快速开发平台!

mldong —— 一个基于 SpringBoot + Vue3 实现的工作流引擎快速开发平台,采用前后端分离的模式,内置完整的权限架构。大家好,我是 Java陈序员。 今天,给大家介绍一个基于 SpringBoot + Vue3 的工作流引擎快速开发平台!关注微信公众号:【Java陈序员】,获取开源项目分享、…

搭建开源笔记平台:outline

折腾的意义 为什么要自己搭建一个笔记平台?没理由,就是突然想试试。有时候突然有个想法,搜了一下正好有合适的方案,就顺手试一下。其实已经有很多成熟的笔记软件,例如Notion/OneNote,但谁不想要一个数据完全在自己服务器的笔记呢。 开始搭建 这个搭建是真的麻烦,需要一堆…

useDeferredValue的作用

前言 useDeferredValue是react18新增的一个用于优化性能的一个hook,它的作用是延迟获取一个值,实际开发中的常规用法与我们之前所用的防抖和节流很相似,但是也有一定的区别。本篇文章我们就逐步分析它的设计原理和用法,并且讨论它与防抖节流的区别和它自身的优势。在讨论us…

Spring AI 增加混元 embedding 向量功能

上次我们讨论了如何将自己的开源项目发布到 Maven 中央仓库,确保其能够方便地被其他开发者使用和集成。而我们的项目 spring-ai-hunyuan 已经具备了正常的聊天对话功能,包括文本聊天和图片理解等基础功能。今天,我们进一步优化和扩展了该项目,新增了一个向量化功能。如图所…