当前位置: 首页 > article >正文

Linux程序性能分析

为什么程序会慢?

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

  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;
        return1;
    }
    
    std::cout << "开始解析日志文件: " << argv[1] << std::endl;
    auto entries = parse_log(argv[1]);
    std::cout << "共发现 " << entries.size() << " 条ERROR级别日志" << std::endl;
    
    // 输出前10条错误日志
    int count = 0;
    for (constauto& entry : entries) {
        if (count++ < 10) {
            std::cout << entry.timestamp << ": " << entry.message << std::endl;
        } else {
            break;
        }
    }
    
    return0;
}

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

$ 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.): 94623200788
  Children      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;
        return1;
    }
    
    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 (constauto& 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;
    
    return0;
}

再次编译运行:

$ 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 找出具体的热点函数

原文地址:https://blog.csdn.net/u013576331/article/details/146494788
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/611416.html

相关文章:

  • CSS3学习教程,从入门到精通,CSS3 图像属性知识点及案例代码(16)
  • 自动化测试selenium(Java版)
  • 深度学习 Note.1
  • 使用Debezium采集Postgresql数据
  • Ubuntu 更换阿里云镜像源图文详细教程
  • Flink基础简介和安装部署
  • 2025.03.23【前沿工具】| CellPhoneDB:基因网络分析与可视化的利器
  • 计算机视觉的多模态模型:开启感知智能的新篇章
  • 《新华网》主流媒体理论论文发表注意事项,有稿费!
  • 施磊老师高级c++(七)
  • 分布式爬虫框架Scrapy-Redis实战指南
  • XYCTF2024 ezSerialize WP
  • 信息安全的数学本质与工程实践
  • SQL中体会多对多
  • Go 语言 fmt 模块的完整方法详解及示例
  • 认识 Express.js:Node.js 最流行的 Web 框架
  • TiDB与Doris实操对比:深度剖析数据库选型要点
  • flutter-实现瀑布流布局及下拉刷新上拉加载更多
  • 外设的中断控制
  • Go 语言 sync 包使用教程