高效Python-2-1 剖析(Profiling 性能分析)

来源:https://www.cnblogs.com/testing-/archive/2023/08/02/17595922.html
-Advertisement-
Play Games

反轉`pandas` `DataFrame`的行列順序是一種非常實用的操作。在實際應用中,當我們需要對數據進行排列或者排序時,通常會使用到Pandas的行列反轉功能。這個過程可以幫助我們更好地理解數據集,發現其中的規律和趨勢。同時,行列反轉還可以幫助我們將數據可視化,使得圖表更加易於理解。 除了常規 ...


2 從內置功能中獲取最高性能

本章包括

  • 剖析代碼以發現速度和記憶體瓶頸
  • 更有效地利用現有的Python數據結構
  • 瞭解Python分配典型數據結構的記憶體成本
  • 使用懶編程技術處理大量數據

有很多工具和庫可以幫助我們編寫更高效的Python。但是,在我們深入研究提高性能的所有外部選項之前,讓我們先仔細看看如何編寫在計算和IO性能方面都更高效的純 Python代碼。事實上,許多Python性能問題(當然不是全部)都可以通過更加註意Python的限制和能力來解決。

為了展示Python自身用於提高性能的工具,讓我們將它們用於一個假設但現實的問題。假設您是一名數據工程師,負責準備對全球氣候數據進行分析。這些數據將基於美國國家海洋和大氣管理局(NOAA;http://mng.bz/ydge )的綜合地表資料庫。您的時間很緊,而且只能使用大部分標準Python。此外,由於預算限制,購買更強的處理能力也是不可能的。數據將在一個月後開始到達,您計劃利用數據到達前的時間來提高代碼性能。因此,您的任務就是找到需要優化的地方並提高其性能。

您要做的第一件事就是對現有的代碼進行剖析,以便攝取數據。你知道現有的代碼速度很慢,但在嘗試優化之前,你需要找到瓶頸的經驗證據。剖析之所以重要,是因為它能讓您以嚴謹、系統的方式搜索代碼中的瓶頸。最常見的替代方法--猜測,在這裡尤其無效,因為許多減速點可能很不直觀。

我們將瞭解純Python提供了哪些開箱即用的功能來幫助我們開發性能更高的代碼。首先,我們將使用幾種剖析工具對代碼進行剖析,以發現問題所在。然後,我們將重點關註Python的基本數據結構:列表、集合和字典。我們的目標是提高這些數據結構的效率,並以最佳方式為它們分配記憶體,以獲得最佳性能。最後,我們將瞭解現代Python懶編程技術如何幫助我們提高數據管道的性能。

本章將主要討論在沒有外部庫的情況下優化Python,但我們仍將使用一些外部工具來幫助我們優化性能和訪問數據。我們將使用Snakeviz來可視化Python剖析的輸出,並使用line_profiler來逐行剖析代碼。最後,我們將使用requests庫從互聯網下載數據。

2.1對具有IO和計算工作負載的應用程式進行剖析

我們的第一個目標是從氣象站下載數據,並獲取該氣象站某一年的最低溫度。NOAA 網站上的數據有 CSV 文件,每個年份一個,然後每個站點一個。例如,文https://www.ncei.noaa.gov/data/global-hourly/access/2021/01494099999.csv 包含01494099999氣象站2021年的所有條目。其中包括溫度和氣壓等條目,每天可能會記錄多次。

讓我們開發一個腳本,下載一組站點在某一年份間隔內的數據。下載相關數據後,我們將得到每個站點的最低氣溫。

2.1.1 下載數據並計算最低氣溫

我們的腳本將有一個簡單的命令行界面,通過該界面傳遞站點列表和感興趣的年份間隔。

執行:

# 獲取站點01044099999和02293099999 2021年的數據
$ python load.py 01044099999,02293099999 2021-2021
{'01044099999': -10.0, '02293099999': -27.6}

源碼

import collections
import csv
import sys

import requests

stations = sys.argv[1].split(",") #站點用逗號分割
years = [int(year) for year in sys.argv[2].split("-")] #年份用區間表示
start_year = years[0]
end_year = years[1]

TEMPLATE_URL = "https://www.ncei.noaa.gov/data/global-hourly/access/{year}/{station}.csv"
TEMPLATE_FILE = "station_{station}_{year}.csv"

def download_data(station, year):
    my_url = TEMPLATE_URL.format(station=station, year=year)
    req = requests.get(my_url)
    if req.status_code != 200:
        return  # not found
    w = open(TEMPLATE_FILE.format(station=station, year=year), "wt")
    w.write(req.text)
    w.close()


def download_all_data(stations, start_year, end_year):
    for station in stations:
        for year in range(start_year, end_year + 1):
            download_data(station, year)


# 用pandas更佳
def get_file_temperatures(file_name):
    with open(file_name, "rt") as f:
        reader = csv.reader(f)
        header = next(reader)
        for row in reader:
            station = row[header.index("STATION")]
            tmp = row[header.index("TMP")]
            temperature, status = tmp.split(",")
            if status != "1":
                continue
            temperature = int(temperature) / 10
            yield temperature


def get_all_temperatures(stations, start_year, end_year):
    temperatures = collections.defaultdict(list)
    for station in stations:
        for year in range(start_year, end_year + 1):
            for temperature in get_file_temperatures(TEMPLATE_FILE.format(station=station, year=year)):
                temperatures[station].append(temperature)
    return temperatures


def get_min_temperatures(all_temperatures):
    return {station: min(temperatures) for station, temperatures in all_temperatures.items()}


download_all_data(stations, start_year, end_year)
all_temperatures = get_all_temperatures(stations, start_year, end_year)
min_temperatures = get_min_temperatures(all_temperatures)
print(min_temperatures)

現在,真正的樂趣開始了。我們的目標是在許多年裡不斷從許多站點下載大量數據。為了處理如此大量的數據,我們希望儘可能提高代碼的效率。提高代碼效率的第一步是有條理地全面剖析代碼,找出拖慢代碼運行的瓶頸。為此,我們將使用Python內置的剖析機制。

2.1.2 Python 內置剖析模塊

我們要確保代碼儘可能高效,首先要做的就是找到代碼中存在的瓶頸。我們首先要做的就是對代碼進行剖析,檢查每個函數的耗時。為此,我們通過cProfile模塊運行代碼。請確保不要使用profile模塊,因為它的速度要慢很多;只有當您自己開發剖析工具時,它才有用。

我們需要的是按累計時間排序的配置文件統計數據。使用該模塊的最簡單方法是在模塊調用中將我們的腳本傳遞給profiler,如下所示:

$ python -m cProfile -s cumulative load.py 01044099999,02293099999 2021-2021 > profile.txt
$ cat profile.txt 
{'01044099999': -10.0, '02293099999': -27.6}
         387321 function calls (381489 primitive calls) in 16.216 seconds		

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    174/1    0.000    0.000   16.216   16.216 {built-in method builtins.exec}
        1    0.000    0.000   16.216   16.216 load.py:1(<module>)
        1    0.000    0.000   16.013   16.013 load.py:25(download_all_data)
        2    0.001    0.000   16.013    8.006 load.py:15(download_data)
        2    0.000    0.000   15.973    7.986 api.py:62(get)
        2    0.000    0.000   15.973    7.986 api.py:14(request)
        2    0.000    0.000   15.972    7.986 sessions.py:500(request)
[...]       
        1    0.000    0.000    0.000    0.000 socks.py:78(SOCKS5AuthError)

在許多情況下I/O有可能在所需時間方面占據主導地位。在我們的例子中,既有網路I/O(從NOAA獲取數據),也有磁碟I/O(將數據寫入磁碟)。由於網路成本取決於沿途的許多連接點,因此即使在不同的運行中,網路成本也會有很大差異。由於網路成本通常是最大的時間損失,因此我們要儘量減少網路成本。

2.1.3 使用本地緩存減少網路使用量

為了減少網路通信,讓我們在首次下載文件時保存一份副本,以備將來使用。我們將建立一個本地數據緩存。除了函數download_all_data外,我們將使用與前面相同的代碼:

def download_all_data(stations, start_year, end_year):
    for station in stations:
        for year in range(start_year, end_year + 1):
            if not os.path.exists(TEMPLATE_FILE.format(station=station, year=year)):
                download_data(station, year)

執行結果:

$ python -m cProfile -s cumulative load_cache.py 01044099999,02293099999 2021-2021 > profile_cache.txt
$ head profile_cache.txt 
{'01044099999': -10.0, '02293099999': -27.6}
         316570 function calls (310825 primitive calls) in 0.187 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    172/1    0.000    0.000    0.187    0.187 {built-in method builtins.exec}
        1    0.000    0.000    0.187    0.187 load_cache.py:1(<module>)
       16    0.000    0.000    0.169    0.011 __init__.py:1(<module>)
        1    0.007    0.007    0.103    0.103 load_cache.py:51(get_all_temperatures)

雖然運行時間縮短了一個數量級,但IO仍然耗時最長。現在,不是網路問題,而是磁碟訪問問題。這主要是由於計算量過低造成的。

緩存管理也可能存在問題,而且是常見錯誤的根源。在我們的示例中,文件從未隨時間發生變化,但緩存的許多用例中,源文件可能會發生變化。在這種情況下,緩存管理代碼需要認識到這個問題。我們將在本書的其他部分再次討論緩存問題。

2.2 剖析代碼以檢測性能瓶頸

在這裡,我們將研究CPU耗時最長的代碼。我們將使用NOAA資料庫中的所有站點計算它們之間的距離,這是一個複雜度為n2的問題。

由於我們要比較所有站點之間的距離,因此複雜度為 n2。
前面的代碼需要很長時間才能運行。同時也會占用大量記憶體。如果您有記憶體問題,請限制要處理的站點數量。現在,讓我們使用 Python 的剖析基礎結構來看看大部分時間都花在哪裡了。

2.2.1 可視化剖析信息

我們再次查找延遲執行的代碼片段。但為了更好地檢查跟蹤,我們將使用外部可視化工具SnakeViz(https://jiffyclub.github.io/snakeviz/)。

# pip install snakeviz
$ python -m cProfile -o distance_cache.prof distance_cache.py

註意Python提供了pstats模塊來分析寫入磁碟的跟蹤信息。您可以執行 python -m pstats distance_cache.prof,這將啟動一個命令行界面來分析我們腳本的代價。有關該模塊的更多信息,請參閱 Python 文檔或第 5 章的剖析部分。

為了分析這些信息,我們將使用網路可視化工具SnakeViz。您只需執行snakeviz distance_cache.prof。這將啟動一個互動式瀏覽器視窗(圖 2.1 顯示了一個截圖)。

image

2.2 行剖析

我們將使用https://github.com/pyutils/line_profiler上。使用行剖析器非常簡單:只需在 get_distance 中添加註解即可:

@profile
def get_distance(p1, p2):

這是因為我們將使用line_profiler軟體包中的便捷腳本kernprof來:

kernprof -l lprofile_distance_cache.py

行剖析器所需的工具會大大降低代碼的運行速度,慢上幾個數量級。如果中斷運行,仍會有跟蹤記錄。剖析器運行結束後,可以使用以下命令查看結果:

$ python -m line_profiler lprofile_distance_cache.py.lprof
Timer unit: 1e-06 s

Total time: 19.194 s
File: lprofile_distance_cache.py
Function: get_distance at line 16

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    16                                           @profile
    17                                           def get_distance(p1, p2):
    18   6285284    1038835.3      0.2      5.4      lat1, lon1 = p1
    19   6285284     942398.6      0.1      4.9      lat2, lon2 = p2
    20                                           
    21   6285284    1425843.5      0.2      7.4      lat_dist = math.radians(lat2 - lat1)
    22   6285284    1342482.5      0.2      7.0      lon_dist = math.radians(lon2 - lon1)
    23   6285284     611137.0      0.1      3.2      a = (
    24   6285284    2646991.4      0.4     13.8          math.sin(lat_dist / 2) * math.sin(lat_dist / 2) +
    25  12570568    3500465.6      0.3     18.2          math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
    26  12570568    2468080.8      0.2     12.9          math.sin(lon_dist / 2) * math.sin(lon_dist / 2)
    27                                               )
    28   6285284    2877574.5      0.5     15.0      c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    29   6285284     725911.4      0.1      3.8      earth_radius = 6371
    30   6285284     950183.7      0.2      5.0      dist = earth_radius * c
    31                                           
    32   6285284     664096.4      0.1      3.5      return dist

參考資料

2.2.3 Profiling小結

正如我們所看到的,作為第一種方法,內置剖析器總體上是一個很大的幫助;它也比行剖析快得多。但是行剖析的信息量要大得多,這主要是因為內置剖析不提供函數內部的細分。相反,Python的剖析只提供每個函數的累計值,並顯示花費在子調用上的時間。在特定情況下,可以知道一個子調用是否屬於另一個函數,但一般來說,這是不可能的。剖析的總體策略需要考慮到所有這些因素。

我們在這裡使用的策略是一種普遍合理的方法:首先,嘗試內置的cProfile,因為它速度快,而且能提供一些高級信息。如果這還不夠,可以使用行剖析,它的信息量更大,但速度也更慢。請記住,在這裡我們主要關註的是找到瓶頸;後面的章節將提供優化代碼的方法。有時,僅僅改變現有解決方案的部分內容是不夠的,還需要進行總體架構重構;我們也會在適當的時候討論這個問題。

timeit可能是新手最常用的代碼剖析方法,你可以在互聯網上找到大量使用 timeit 模塊的示例。使用timeit模塊最簡單的方法是使用IPython或Jupyter Notebook,因為這些系統能讓timeit非常精簡。例如,在IPython中,只需將%timeit魔法添加到你想要剖析的內容中即可:

In [1]: %timeit list(range(1000000))
18.5 ms ± 37.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [2]: %timeit range(1000000)
82.1 ns ± 0.721 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

這將為您提供正在剖析的函數的多次運行時間。這個魔法將決定運行多少次並報告基本統計信息。在前面的代碼段中,你可以看到range(1000000)和list(range(1000000))的區別。在這個具體案例中,timeit顯示,range的懶惰版本比急切版本快兩個數量級。

你可以在timeit模塊的文檔中找到更多細節,但在大多數情況下,使用IPython的%timeit功能就足夠了。我們鼓勵你使用IPython及其魔法,但在本書的其他大部分內容中,我們將使用標準解釋器。有關%timeit魔法的更多信息,請訪問:https://ipython.readthedocs.io/en/stable/interactive/magics.html。

釘釘或微信號: pythontesting 微信公眾號:pythontesting
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文分享自華為雲社區《如何為物聯網設備註入“華為雲+鴻蒙DNA”?看華為雲IoT怎麼答【華為雲IoT +鴻蒙】》,作者: 華為IoT雲服務。 根據市場咨詢機構預測,2025年全球物聯網設備將達到252億個。但各種智能設備大多都有一套自己的系統,而且互相“孤立”,無法交流。鴻蒙的到來,就是要用同一套語 ...
  • 由於Android APP/IOS APP平臺和開發語言的差異,對開髮端和用戶端來說,在系統相容適配、外接藍牙的安裝更新,以及不同平臺之間的移植都有不同程度的制約。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 介紹 canvas:使用腳本 (通常為 JavaScript) 來繪製圖形的 HTML 元素。 本人遍歷了以下兩份文檔,學習完就相當於有了筆和紙,至於最後能畫出什麼,則需要在 canvas 應用方面進一步學習。 MDN 的 Canvas ...
  • ### 1. 使用箭頭函數簡化函數定義 ```cobol // 傳統函數定義 function add(a, b) { return a + b; } // 箭頭函數簡化 const add = (a, b) => a + b; ``` ### 2. 使用解構賦值簡化變數聲明 ```cobol // ...
  • 1、html的基本結構 a.首先是註釋信息,在html中使用<!--xxx-->這樣的方式來進行註釋 b.DOCTYPE,這是告訴瀏覽器所使用規範,一般可以不加這個,因為現在的瀏覽器預設所使用的規範為html c.head標簽,表示網頁的頭部,其中會放入其他標簽 d.meta標簽,這是一個描述性的標 ...
  • springboot項目通常配合mybatisplus來做數據CRUD。 我們在查詢或更新數據的時候,有時要用到in來過濾數據。比如SELECT * FROM emax_scbg_order WHERE order_no IN (1305679009380433922,130540525947283 ...
  • # ctfshow--web入門--文件上傳 [TOC](目錄標題) ## web151(前端校驗) 題目中提示前端檢驗不可靠,應該對前端檢驗進行繞過 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/608e51750ded43f2aefc1c2a66a3e803. ...
  • ## 微服務 Spring Microservices是一個框架,它使用Spring框架更容易地構建和管理基於微服務的應用程式。微服務是一種架構風格,其中一個大型應用程式被構建為一組小型、獨立可部署的服務。每個服務具有明確定義的職責,並通過API與其他服務通信。 ### 雲配置 Spring Clo ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...