太阳说,用Python cProfile找出瓶颈函数!
我们!
呜!
过来!
喵!
大家好,我是无能。
最近一边玩电脑一边听着前HAL研究所出身的樱井政博先生的频道,感觉非常愉快。
说起来,玩3DS的记忆也复苏了,当时看的岩田社长的任天堂直面会总是那么有趣。重新审视,如果说其他人,比如富士通的池田敏雄,看他们的纪录片会感受到巨大的热情,在那个时代多次前往美国,简直是不可思议的世界。当时还有越南战争,二战后的阴影应该还在,但那股能量究竟是从何而来的呢?
前言
我之前对脚本语言的执行速度不太在意,但在运行自己开发的cuckooget时,却不自觉地开始关注起来。
部分代码是用Rust编写的,可以通过PyO3/maturin在解释器中执行Rust代码。
简单来说,就是将Rust编写的函数封装成Python库,通过编译后的Rust二进制文件来执行。对于CPU密集型运算,这样做会更快,这是个简单的理由。结果确实变快了,但这其中有一个很大的错误。
首先,我根本没有对纯Python代码进行基准测试就进行了测量。
在被称为Go之父的罗布·派克所著的《C编程备忘录》中也提到:
规则1:你无法知道程序会在哪里消耗时间。瓶颈会出现在令人惊讶的地方。因此,在明确瓶颈所在之前,不要进行猜测或进行速度优化。
既然如此,就应该进行测量。
如果低层C语言的原则是这样,那么它不可能不适用于更高层的语言。
我有时会忘记这些,所以深感需要像UNIX思想那样定期进行检查,以此来约束自己。
话虽如此,但作为人类,我虽然明白,却总是容易做出异常值……。
使用cProfile进行测量
最初我用的是原始方法,从import time开始,在函数执行结束的start_time = time.time()和end_time = time.time()之后,用print(f"exec time {start_time - end_time}")来计算时间。但既然都用电脑了,这种效率也太低了!于是我开始寻找能显示每个函数执行时间的方法。
据说有一个名为cProfile的标准库。
以下网站解释得很清楚:
使用 Python cProfile 分析代码性能
我的情况如下确认。
python3 -m cProfile -o profile.pstats ./main.py https://soulminingrig.com/ site
这样,./main.py 执行的函数所花费的时间将存储在名为profile.pstats的二进制文件中。
查看函数执行结果
刚才的网站是使用Python可执行文件进行的,但更方便的方法是进入pstats的命令行模式进行查看。
python3 -m pstats profile.pstats
这样就进入了pstats的命令行模式,可以通过以下方式查看输出结果。
profile.pstats% stats 5
Thu Nov 7 20:24:10 2024 profile.pstats
48488696 function calls (47625490 primitive calls) in 51.151 seconds
Random listing order was used
List reduced from 3017 to 5 due to restriction <5>
ncalls tottime percall cumtime percall filename:lineno(function)
1586 0.001 0.000 0.001 0.000 /home/haturatu/.local/lib/python3.12/site-packages/aiohttp/client_reqrep.py:909(__del__)
80 0.000 0.000 0.001 0.000 /usr/lib/python3.12/copy.py:247(_reconstruct)
4 0.000 0.000 0.005 0.001 /usr/lib/python3.12/encodings/__init__.py:71(search_function)
3 0.000 0.000 98.745 32.915 /usr/lib/python3.12/asyncio/base_events.py:651(run_until_complete)
1103 0.001 0.000 0.003 0.000 /usr/lib/python3.12/re/_parser.py:312(_class_escape)
可以通过help查看其他命令。
profile.pstats% help
Documented commands (type help <topic>):
========================================
EOF add callees callers help quit read reverse sort stats strip
我的情况是,输出stats 100,然后从另一个打开的终端用vim随意打开并复制粘贴,然后quit了。
对于粘贴到vim并保存的文本文件,我将分别使用awk和sort进行查看。
tottime 的情况
$ awk '{ print $2","$6 }' bench | sort | column -t -s "," | tail
0.009 /usr/lib/python3.12/re/_compiler.py:243(_optimize_charset)
0.010 /usr/lib/python3.12/concurrent/futures/_base.py:428(result)
0.012 /usr/lib/python3.12/asyncio/base_events.py:627(run_forever)
0.013 /usr/lib/python3.12/re/__init__.py:280(_compile)
0.018 /usr/lib/python3.12/re/_compiler.py:37(_compile)
0.022 /usr/lib/python3.12/concurrent/futures/_base.py:497(set_running_or_notify_cancel)
0.030 /home/haturatu/git/devgit/async_web_mirror.py:186(download_and_save_image)
0.031 /usr/lib/python3.12/re/_parser.py:512(_parse)
0.061 /usr/lib/python3.12/concurrent/futures/_base.py:537(set_result)
tottime filename:lineno(function)
总觉得,仅凭tottime,作为每个函数的基准信息似乎已经足够了。
Beautiful Soup 4 的瓶颈是错觉吗?
在其他环境中发现的慢点似乎是bs4调用的element.py中的search_tag拖了后腿。
我注意到的是这里:
if ((not self.name)
or call_function_with_tag_data
or (markup and self._matches(markup, self.name))
or (not markup and self._matches(markup_name, self.name))):
很难相信or运算会成为瓶颈……。
然而,在element.py中,使用or进行评估的地方只有7处,其中3处用在了这里。
总觉得这里有点问题。
所以,话题转向了bs4,就到这里吧。
下次再见。