日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

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

發布時間:2024/1/23 python 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 使用 cProfile 和火焰图调优 Python 程序性能 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前幾天調試程序,發現 QPS 總是卡在 20 左右上不去。開始以為是 IO 問題,就多開了些并發,然并卵,這才想到可能是 CPU 的問題。看了看監控,發現程序某一步的延遲在 400ms 左右,而且這一步是 CPU 密集的。當時開了 4 臺雙核的機器:(1s / 400ms) * 2 * 4 = 20 啊。看來需要優化下這一步的代碼了,那么第一步就是找到可以優化的地方。

測量程序的性能之前并沒有實際做過,Google 了一番,感覺標準庫的 cProfile 似乎值得一試。

要測量的代碼邏輯也很簡單,使用 lxml 解析 HTML,然后提取一些字段出來,這些基本都是調用的 C 庫了,解析的算法也不在 Python 中。看起來似乎沒有什么能改進的地方,不管怎樣,還是先跑一下吧。

cProfile 有多種調用方法,可以直接從命令行調用:

python -m cProfile -s tottime your_program.py

其中的?-s?的意思是 sort。常用的 sort 類型有兩個:

  • tottime,指的是函數本身的運行時間,扣除了子函數的運行時間
  • cumtime,指的是函數的累計運行時間,包含了子函數的運行時間
  • 要獲得對程序性能的全面理解,經常需要兩個指標都看一下。

    不過在這里,我們并不能直接使用命令行方式調用,因為我的代碼中還需要一些比較繁重的配置加載,如果把這部分時間算進去了,多少有些干擾,那么我們也可以直接在代碼中調用 cProfile。

    使用 cProfile 的代碼如下:

    import cProfile, pstats, iopr = 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 排序,也就是累計運行時間。

    結果如下:

    我們可以看到總的運行時間是 200ms,而其中紅框內的部分就占了 100ms! 如果能夠優化調的話,性能一下子就能提高一倍。紅框內的代碼是做什么的呢?我們知道解析一個 html 文檔,第一步是建立 DOM 樹,通常情況下,我們可能會從其中抽取一些鏈接。在網頁中,鏈接不一定是絕對路徑,也可能是?/images/2018-12-31-xxx.jpg?這樣的相對路徑。lxml 庫幫我們做了一個貼心的默認值,那就是在構造 DOM 樹的時候,根據傳入的 url 來吧頁面中的所有 url 都重寫成絕對路徑。看起來這是個很貼心的功能,但是在這里卻成了性能瓶頸。為什么很耗時呢?大概是因為需要遍歷整個 DOM 樹,重寫所有的鏈接吧。這顯然不是我們需要的,我們只需要把抽取之后的鏈接還原成絕對路徑就好了,而不是事先把所有鏈接都重寫一遍。所以在這里我們直接去掉這個功能就好了。

    修改代碼之后,再次運行 profile 腳本,時長變成了 100ms:

    這時候我們接著看,程序中下一個比較大頭的時間占用:jsonfinder 和 json decode/encode。

    jsonfinder 是一個有意思的庫,它自動從 HTML 中查找 json 字符串并嘗試解析,但是有時候也不太準。經常為了找到特定的值,還是需要使用正則。那么對于這個可有可無的功能,性能有這么差,還是刪掉好了。

    通過刪代碼,現在性能已經是原來的四倍了。

    這時候發現代碼里面有正則還挺花費時間的,不過還好,暫時先不管了。

    剛剛都是只運行了一遍,測量結果難免有隨機性,必定有失偏頗,實際上應該使用多個測試用例,成千上萬次的跑,才能得到一個比較準確地結果。

    上面這個小步驟基本沒有什么可以優化的了,下面我們把優化目標擴大一點,并把次數先定為100.

    下面這種圖是按照 tottime 來排序的:

    注意其中最耗時的步驟是 parseUnicodeDoc,也就是建樹了,這是符合預期的,然而旁邊的 ncalls 一欄卻不太對勁了。我們明明只運行了 100 次,為什么這個函數調用了 300 次呢?顯然代碼中有重復建樹的地方,也就是有隱藏的 bug。這種地方不經過 profile 很難浮現出來,因為程序本身的邏輯是對的,只是比較耗時而已。

    優化之后,終于變成了 100. 從 cProfile 的表格現在已經看不出什么結果來了,下一步我們開始使用火焰圖,可視化往往能讓我們更容易注視到性能瓶頸。(為什么不一開始就用火焰圖呢?因為我以為很麻煩。。實際很簡單)

    Python 中有一個第三方包(見參考文獻)可以直接從 cProfile 的結果生成火焰圖:

  • 在原有的代碼中加上一句:?pr.dump_stats("pipeline.prof")
  • 調用該工具:flameprof pipeline.prof > pipeline.svg
  • 然后打開 SVG 文件就可以了:

    其中火焰的寬度代表了運行的時長,我們現在的優化目標就是這些耗時比較長的步驟。

    可以看大其中 mysql 的訪問占了絕對的大頭,按理說跑100次的話,不應該每次都花費時間在建立連接上啊,這里一定有問題。經過排查發現在某處鏈接是使用了?close_old_connections?來保證不會拋出數據庫斷開的異常,這還是在頭條帶來的習慣。。close_old_connections 的功能是關閉已經失效的鏈接,看來我的理解還是有誤的。先把這塊刪掉,最終解決應該是這塊放到一個隊列里,統一存入數據庫。

    去掉之后:

    現在的大頭又變成 lxml 的,又動了優化它的心思,lxml 是 libxml2 的一個 Python binding,查了下應該是最快的 html parser 了,這塊真的沒有什么優化空間。盯了一會兒,眼睛最終看到了一個小角落:

    一個正則匹配居然占用了 8% 的運行時間,太不像話了。老早之前就聽說 Python 的標準庫正則性能不行,現在才發現原來是真的挺差勁的。Python 標準庫的 re 模塊采用的是 PCRE 的處理方式,而采用 NFA 的處理方式的正則要快很多,這塊還需要再看一下。不過眼下倒是可以直接換一個庫來解決。regex 模塊是 re 模塊的一個 drop-in replacement.

    pip install regex?and?import regex as re,就搞定了

    可以看到正則那塊直接消失了。提升還是很大的。時間不早了,當天的優化就到此結束了。上線之后,積壓一下子就下去了:

    后記

    要想調試的時候方便,在寫代碼的時候就要注意,盡量使自己的代碼 mock-friendly 一點。如果需要引入外部的數據庫、服務、API等等各種資源,最好有一個開關或者選項能夠不加載外部資源,或者至少能夠很方便地 mock 這些外部服務,這樣方便對每一個小單元進行 profile。

    總有人吐槽 Python 的性能低下,但是 Python 本來就不是做計算任務的呀,Python 是一門膠水語言,是用來寫業務邏輯的,而不是用來寫CPU密集的算法的。事實上復雜的解析一般都會用 C++ 這種硬核語言來寫了,比如 numpy TensorFlow lxml。大多數程序員一天 90% 的工作除了和產品經理撕逼以外,也就是在寫 CRUD,也就是調用這些包。所以瓶頸一般在 IO 上而不在 CPU 上,而解決 IO 的瓶頸手段就多了,Python 中至少有 多進程、多線程、AsyncIO、Gevent 等多種方法。不過方法多其實也是一個弊端,這幾種方法可以說是基本互不兼容,對各種第三方庫的支持也參差不齊。而 Go 在這方面就做地很好了,語言直接內置了 go 關鍵字,甚至都不支持多線程。所有的庫都是支持一個統一的并發模型,對于使用者來說更簡單。

    Zen of Python 中有一句:There should be one way -- preferably only one way -- to do a thing. 這點上 Python 本身沒有做到,反倒是 Go 實踐地非常好。

    扯遠了,程序的瓶頸其實不外乎CPU、內存和 IO 三個方面,而 cProfile 和火焰圖是判斷 CPU 瓶頸的一把利器。

    后面還發現了一些性能瓶頸,也列在這里:

  • yaml 的反序列化時間過長。解決方法是添加了一個 Expiring LRU Cache,不要每次都去加載,當然犧牲的是一點點內存,以及當規則變更時會有一些延遲,不過都是可以接受的。之前早就聽人說 Thrift 的序列化性能相比 Protobuf 太低,現在想想序列化和反序列化還真是一個很常見的性能瓶頸啊。
  • 存儲使用了 360 的 pika,pika 可以理解為一個基于 rocksdb 的硬盤版 redis。最開始的時候沒多想,隨便找了臺機器搭了起開,把上面的問題解決之后,pika 的延遲很快大了起來,機器的監控也顯示 IO 基本被打滿了。這時候才發現原來這臺機器沒有用 SSD,果斷換了 SSD 問題基本解決了。如果再有問題可能就需要集群了。
  • 性能這個問題其實是典型的木桶理論的場景,系統的整體性能是由最差的一塊決定的。所以也是一個不斷迭代的過程。

    祝大家新年快樂~

    參考文獻

  • https://toucantoco.com/en/tech-blog/tech/python-performance-optimization
  • https://docs.python.org/3.6/library/profile.html
  • https://medium.com/build-smarter/blazing-fast-python-40a2b25b0495
  • https://swtch.com/~rsc/regexp/regexp1.html
  • 總結

    以上是生活随笔為你收集整理的使用 cProfile 和火焰图调优 Python 程序性能的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。