使用 cProfile 和火焰图调优 Python 程序性能(上)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 使用 cProfile 和火焰图调优 Python 程序性能

前几天调试程序,发现 QPS 总是卡在 20 左右上不去。开始以为是 IO 问题,就多开了些并发,然并卵,这才想到可能是 CPU 的问题。看了看监控,发现程序某一步的延迟在 400ms 左右,而且这一步是 CPU 密集的。当时开了 4 台双核的机器:(1s / 400ms) * 2 * 4 = 20 啊。看来需要优化下这一步的代码了,那么第一步就是找到可以优化的地方640.jpg测量程序的性能之前并没有实际做过,Google 了一番,感觉标准库的 cProfile 似乎值得一试。

要测量的代码逻辑也很简单,使用 lxml 解析 HTML,然后提取一些字段出来,这些基本都是调用的 C 库了,解析的算法也不在 Python 中。看起来似乎没有什么能改进的地方,不管怎样,还是先跑一下吧。

cProfile 有多种调用方法,可以直接从命令行调用:

python -m cProfile -s tottime your_program.py

其中的 -s 的意思是 sort。常用的 sort 类型有两个:

  1. tottime,指的是函数本身的运行时间,扣除了子函数的运行时间
  2. cumtime,指的是函数的累计运行时间,包含了子函数的运行时间

要获得对程序性能的全面理解,经常需要两个指标都看一下。

不过在这里,我们并不能直接使用命令行方式调用,因为我的代码中还需要一些比较繁重的配置加载,如果把这部分时间算进去了,多少有些干扰,那么我们也可以直接在代码中调用 cProfile。

使用 cProfile 的代码如下:

import cProfile, pstats, io
pr = cProfile.Profile()
pr.enable()
extractor.extract(crawl_doc=doc, composition=PageComposition.row, rule=rule)
pr.disable()
s = io.StringIO()sortby = "cumtime" # 仅适用于 3.6, 3.7 把这里改成常量了
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print(s.getvalue())

把需要 profile 的代码放到 pr.enable 和 pr.disable 中间就好了。注意这里我们使用了 cumtime 排序,也就是累计运行时间。

结果如下:640 (1).jpg我们可以看到总的运行时间是 200ms,而其中红框内的部分就占了 100ms! 如果能够优化调的话,性能一下子就能提高一倍。红框内的代码是做什么的呢?我们知道解析一个 html 文档,第一步是建立 DOM 树,通常情况下,我们可能会从其中抽取一些链接。在网页中,链接不一定是绝对路径,也可能是 /images/2018-12-31-xxx.jpg 这样的相对路径。lxml 库帮我们做了一个贴心的默认值,那就是在构造 DOM 树的时候,根据传入的 url 来吧页面中的所有 url 都重写成绝对路径。看起来这是个很贴心的功能,但是在这里却成了性能瓶颈。为什么很耗时呢?大概是因为需要遍历整个 DOM 树,重写所有的链接吧。这显然不是我们需要的,我们只需要把抽取之后的链接还原成绝对路径就好了,而不是事先把所有链接都重写一遍。所以在这里我们直接去掉这个功能就好了。

修改代码之后,再次运行 profile 脚本,时长变成了 100ms:640 (2).jpg这时候我们接着看,程序中下一个比较大头的时间占用:jsonfinder 和 json decode/encode。

jsonfinder 是一个有意思的库,它自动从 HTML 中查找 json 字符串并尝试解析,但是有时候也不太准。经常为了找到特定的值,还是需要使用正则。那么对于这个可有可无的功能,性能有这么差,还是删掉好了。

通过删代码,现在性能已经是原来的四倍了。

这时候发现代码里面有正则还挺花费时间的,不过还好,暂时先不管了。640 (3).jpg刚刚都是只运行了一遍,测量结果难免有随机性,必定有失偏颇,实际上应该使用多个测试用例,成千上万次的跑,才能得到一个比较准确地结果。

上面这个小步骤基本没有什么可以优化的了,下面我们把优化目标扩大一点,并把次数先定为100.

下面这种图是按照 tottime 来排序的:640 (4).jpg注意其中最耗时的步骤是 parseUnicodeDoc,也就是建树了,这是符合预期的。然而旁边的 ncalls 一栏却不太对劲了。我们明明只运行了 100 次,为什么这个函数调用了 300 次呢?显然代码中有重复建树的地方,也就是有隐藏的 bug。这种地方不经过 profile 很难浮现出来,因为程序本身的逻辑是对的,只是比较耗时而已。640 (5).jpg优化之后,终于变成了 100. 从 cProfile 的表格现在已经看不出什么结果来了,下一步我们开始使用火焰图,可视化往往能让我们更容易注视到性能瓶颈。(为什么不一开始就用火焰图呢?因为我以为很麻烦。。实际很简单)

目录
相关文章
|
1月前
|
机器学习/深度学习 Python
堆叠集成策略的原理、实现方法及Python应用。堆叠通过多层模型组合,先用不同基础模型生成预测,再用元学习器整合这些预测,提升模型性能
本文深入探讨了堆叠集成策略的原理、实现方法及Python应用。堆叠通过多层模型组合,先用不同基础模型生成预测,再用元学习器整合这些预测,提升模型性能。文章详细介绍了堆叠的实现步骤,包括数据准备、基础模型训练、新训练集构建及元学习器训练,并讨论了其优缺点。
50 3
|
1月前
|
机器学习/深度学习 数据挖掘 Python
Python编程入门——从零开始构建你的第一个程序
【10月更文挑战第39天】本文将带你走进Python的世界,通过简单易懂的语言和实际的代码示例,让你快速掌握Python的基础语法。无论你是编程新手还是想学习新语言的老手,这篇文章都能为你提供有价值的信息。我们将从变量、数据类型、控制结构等基本概念入手,逐步过渡到函数、模块等高级特性,最后通过一个综合示例来巩固所学知识。让我们一起开启Python编程之旅吧!
|
1月前
|
存储 Python
Python编程入门:打造你的第一个程序
【10月更文挑战第39天】在数字时代的浪潮中,掌握编程技能如同掌握了一门新时代的语言。本文将引导你步入Python编程的奇妙世界,从零基础出发,一步步构建你的第一个程序。我们将探索编程的基本概念,通过简单示例理解变量、数据类型和控制结构,最终实现一个简单的猜数字游戏。这不仅是一段代码的旅程,更是逻辑思维和问题解决能力的锻炼之旅。准备好了吗?让我们开始吧!
|
9天前
|
安全 API C语言
Python程序的安全逆向(关于我的OPENAI的APIkey是如何被盗的)
本文介绍了如何使用C语言编写一个简单的文件加解密程序,并讨论了如何为编译后的软件添加图标。此外,文章还探讨了Python的.pyc、.pyd等文件的原理,以及如何生成和使用.pyd文件来增强代码的安全性。通过视频和教程,作者详细讲解了生成.pyd文件的过程,并分享了逆向分析.pyd文件的方法。最后,文章提到可以通过定制Python解释器来进一步保护源代码。
37 6
|
22天前
|
IDE 程序员 开发工具
Python编程入门:打造你的第一个程序
迈出编程的第一步,就像在未知的海洋中航行。本文是你启航的指南针,带你了解Python这门语言的魅力所在,并手把手教你构建第一个属于自己的程序。从安装环境到编写代码,我们将一步步走过这段旅程。准备好了吗?让我们开始吧!
|
3天前
|
Shell 开发工具 Python
如何在vim里直接运行python程序
如何在vim里直接运行python程序
|
1月前
|
开发者 Python
使用Python实现自动化邮件通知:当长时程序运行结束时
本文介绍了如何使用Python实现自动化邮件通知功能,当长时间运行的程序完成后自动发送邮件通知。主要内容包括:项目背景、设置SMTP服务、编写邮件发送函数、连接SMTP服务器、发送邮件及异常处理等步骤。通过这些步骤,可以有效提高工作效率,避免长时间等待程序结果。
66 9
|
1月前
|
存储 人工智能 数据挖掘
Python编程入门:打造你的第一个程序
本文旨在为初学者提供Python编程的初步指导,通过介绍Python语言的基础概念、开发环境的搭建以及一个简单的代码示例,帮助读者快速入门。文章将引导你理解编程思维,学会如何编写、运行和调试Python代码,从而开启编程之旅。
42 2
|
1月前
|
Python
在Python中,`try...except`语句用于捕获和处理程序运行时的异常
在Python中,`try...except`语句用于捕获和处理程序运行时的异常
49 5
|
1月前
|
存储 大数据 Python
利用Python的高级语法优化代码可以显著提高代码的可读性、简洁性和性能
利用Python的高级语法优化代码可以显著提高代码的可读性、简洁性和性能
35 1