
6個庫讓你寫出同步代碼跑出異步速度摘要性能優(yōu)化的新思路很長時間以來, 在我們談及性能之際, 有一個無法避開的話題便是多線程。面對數(shù)據(jù)處理、網(wǎng)絡爬取或者I/O密集型任務所存在的性能瓶頸, 好多開發(fā)者會下意識地尋求多線程方案, 指望借助并發(fā)去加快腳本運行。然而, 存在的全局解釋器鎖GIL以及由此產(chǎn)生的上下文切換開銷, 常常致使多線程代碼不但沒有帶來預期中的性能提高, 反而有可能成為性能的“瓶頸”或者“拖累”??墒? 時代已然發(fā)生了變化, 當下, 一些具革命性的庫正起著改變這一情形的作用。存在這樣一些庫, 它們于底層展開了深度優(yōu)化工作, 優(yōu)化所利用的常常是憑借Rust、C語言來達成, 或者是運用高效的異步I/O模型這種方式, 進而致使它們原生的執(zhí)行速度快到了足以將手動管理并發(fā)的必要性給抵消掉的程度。這些庫讓開發(fā)者編寫呈現(xiàn)直觀、同步特點的代碼, 然而卻能夠以如同呈現(xiàn)“暴風雪”般的速度去處理數(shù)據(jù)。這兒會深度講述那六個致使作者徹底摒棄多線程的庫或者工具, 并且闡釋它們怎樣借由底層的機制達成性能的迅猛提升, 給你鋪就一條在不增添代碼繁雜程度的狀況下, 極大提高性能的全新道路句號。一、性能瓶頸的傳統(tǒng)解法與新挑戰(zhàn)多線程的困境于深入知悉這般高性能庫之前, 我們首先得弄明白為何其中的多線程并非那能解決一切問題, 的藥。的性能硬傷GIL與上下文切換的性能并不以“原始速度”著稱。這主要歸咎于兩個核心問題一個名為全局解釋器鎖的裝置也就是 GIL, 它作出這種確保, 即在任意給定的時刻之內(nèi), 僅僅會有單獨的一個線程能夠于虛擬機里執(zhí)行字節(jié)碼。而這所蘊含的意義在于, 針對于那種CPU屬于密集型的任務而言, 多線程是沒有辦法去借助多核所具備的優(yōu)勢的, 相反地, 會由于GIL所帶來的限制以及線程之間非常頻繁的切換也就是上下文切換所產(chǎn)生的開銷, 從而致使速度被拖慢。切換上下文所產(chǎn)生的開銷: 即便置身于I/O密集型任務里, 多線程盡管能夠起到作用緣由在于當一個線程正等候I/O之際, GIL能夠釋放予另一個線程, 然而管理線程、同步數(shù)據(jù)像是鎖、信號量以及線程自身的切換均會造成不容小覷的開銷。以往的時候, 在腳本運行遲緩之際, 我們時常本能地去求助多線程, 以此來應對數(shù)據(jù)處置、網(wǎng)頁信息采集或者輸入輸出密集型工作。然而, 就如同作者所領(lǐng)悟到的那種具有革新性質(zhì)的知識表明: 有的情況下, 你壓根兒不一定要借助線程去達成速度提升。新時代的解決方案庫內(nèi)嵌的并行優(yōu)化高性能庫問世了, 實際上是這般模樣, 把并發(fā)方面存在的復雜性, 從應用領(lǐng)域這一層面, 推到了庫的基底之處。它們借助下面這些方式達成令人驚嘆的速度:下面我們將逐一介紹這六個“高性能黑科技”。二、數(shù)據(jù)分析的“一級方程式”告別的慢速困境對于那些從事處理大規(guī)模數(shù)據(jù)集工作的開發(fā)者而言, 這屬于標準配置, 然而它的性能瓶頸正愈發(fā)明顯地呈現(xiàn)出來。它的出現(xiàn), 就好像是從“家庭轎車升級至F1賽車”這種情況。它是一款依據(jù)Rust構(gòu)建而成的庫, 專門針對性能以及并行化進行了優(yōu)化。它的API和別的類似, 極為便于上手, 不過它在無需手動管理線程的情況下就能達成極高的速度。為何如此之快的速度優(yōu)勢來源于其底層設(shè)計和實現(xiàn)機制簡單示例自動并行化操作使用時開發(fā)者只需編寫簡潔的鏈式同步代碼如下所示import polars as pl df pl.read_csv(sales.csv) result ( df.lazy() .filter(pl.col(region) Europe) .group_by(product) .agg(pl.col(revenue).sum()) .collect() )這段代碼執(zhí)行的速度, 相較于多線程而言會更快, 這是由于它于內(nèi)部自動地對所有操作進行了并行化處理。從而開發(fā)者獲取到了高性能, 然而卻并不需要去管理任何線程。應用場景三, JSON解析里有個“閃電俠”, 它是內(nèi)置JSON模塊, 存在性能短板。要是你曾經(jīng)處理過那種規(guī)模很大的JSON文件, 或者做過數(shù)量眾多的JSON序列化以及反序列化相關(guān)操作, 那你肯定特別清楚內(nèi)置的json模塊到底有多遲緩。它是專為這個而產(chǎn)生的, 它是一個借助Rust予以實現(xiàn)的JSON庫, 它的性能相較于內(nèi)置的json模塊或者ujson要快上2至10倍。極速背后的秘密通過以下優(yōu)化實現(xiàn)了性能的碾壓簡單示例毫秒級完成大型JSON解析曾需要多線程來解析的那種龐大的JSON文件, 眼下借助其一實現(xiàn)了在毫秒級能夠同步達成做完:import orjson data {user: Aashish, score: 99} serialized orjson.dumps(data) deserialized orjson.loads(serialized)異步I/O的“增壓器”, 其應用場景的第四個方面, 是默認事件循環(huán)存在性能局限。面向I/O密集型任務而言的庫, 以及其內(nèi)部所設(shè)置的事件循環(huán), 是一種極好且相當優(yōu)異卓越的解決辦法。然而, 我們原本能夠施展拳腳的更大潛力, 卻是可以做得更棒、更為精巧妥善周全妥善, 以超越這一情況的。是一個替代品, 可用于事件循環(huán), 它基于libuv構(gòu)建, libuv它是Node.js所依賴的同一個C語言庫。的性能提升效果作者把它替換進而置于后端之時, 響應時間出現(xiàn)了下降, 降至原來的40%到50%這個幅度范圍, 整個此過程, 既不需要增添線程, 又不需要額外的代碼。為何能大幅提速簡單示例一行代碼實現(xiàn)性能飛躍只需一行代碼, 就能把默認循環(huán)替換成, 使你的異步代碼立刻“裝上超速引擎”:import asyncio import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())應用場景五, 存在一種無需對原代碼做修改的“加速魔法”, 即為PyPy運行時, 并不去更換代碼, 而僅僅是更換其運行的環(huán)境。偶爾, 增強性能的最終訣竅并非更換一個庫, 而是更換一個運行之時的環(huán)境。存在一種即時編譯JIT的實現(xiàn), 此為PyPy。對于CPU密集類型的代碼而言, 它可促成兩倍至四倍的速度提升, 有可能還遠超這個范圍 , 還有更多。PyPy的加速機制適用條件要是你的代碼屬于純粹編寫, 同時對有關(guān)C語言擴展的依賴程度不高, 那么PyPy會明顯戰(zhàn)勝標準的運行時。簡單示例直接運行獲得速度只需用pypy3命令替換來運行你現(xiàn)有的腳本pypy3 my_script.py就這么簡單無需多線程無需異步只有速度。場景六, PyPy的應用講的是函數(shù)級的“原生指令”, 它是Numba科學計算時性能方面至關(guān)重要的工具。Numba是個堪稱“作弊神器”之物件, 它具備把函數(shù)編譯為經(jīng)優(yōu)化后機器碼之本事。僅僅添加一個裝飾器, 開發(fā)者便能使得字節(jié)碼, 在LLVM那樣一種流行的編譯器基礎(chǔ)設(shè)施之下, 被編譯成為達到優(yōu)化目的和滿足需要條件的那種運行原本編碼指令。這里的LLVM便是流行的被稱為編譯器基礎(chǔ)設(shè)施的東西。Numba的加速原理簡單示例一個裝飾器實現(xiàn)數(shù)十倍加速只需在函數(shù)前添加njit裝飾器from numba import njit import numpy as np njit def compute(arr): total 0 for x in arr: total x ** 2 return total print(compute(np.arange(1_000_000)))相較純的情況而言, 這段代碼的運行速度要比僅僅是純的那種快上數(shù)十倍, 同樣地此代碼的運行過程中, 并不需要線程, 也不需要異步, 僅僅存在著一個裝飾器。Numba的應用場景七, 分布式計算的那種“無感擴展”, Ray告別了手動管理并發(fā)會有的痛苦。Ray是這樣一個庫, 針對分布式計算專門得到布置, 然而就算停于單打獨斗的機器條件之下, 它依舊能夠供應出超乎尋常等級的卓有成效功效結(jié)果。Ray借助直觀的API, 把底層的線程隱藏起來, 把底層的進程進行抽象, 甚至還把底層的分布式節(jié)點給藏起來屏蔽掉。Ray的效率優(yōu)勢Ray讓開發(fā)者能夠去定義actor也就是持久化對象, 還能定義task即無狀態(tài)函數(shù), 這些任務以及actor會自動在多核環(huán)境下, 甚至是在多臺機器上展開擴展以及調(diào)度工作, 它給出了一種取代手動處理并發(fā)的辦法。簡單示例自動并行任務執(zhí)行會自動并行執(zhí)行所有任務的是下面的這段代碼片段, 開發(fā)者對于任何線程邏輯、鎖或者同步問題都不需要去關(guān)心。import ray ray.init() ray.remote def square(x): return x * x results ray.get([square.remote(i) for i in range(1_000)])Ray的應用場景八、為什么是時候放棄手動多線程了多線程, 于理論層面而言, 具備很強的特性, 然而, 在實際開展操作期間, 卻給開發(fā)環(huán)節(jié)以及維護方面帶來了諸多的負擔:有競態(tài)條件, 也就是Race, 還有死鎖, 其調(diào)試過程復雜, 存在GIL的限制, 并且有著高昂的維護成本。猶如本文所闡述的那般, 現(xiàn)代的庫, 尤其是那些借助Rust、C或者異步I/O的庫, 把并發(fā)轉(zhuǎn)變成了隱式的狀態(tài)。它們將性能的提升推進到了庫的內(nèi)部實現(xiàn)層面這兒, 使得開發(fā)者能夠?qū)W⒂跇I(yè)務邏輯部分, 而并非同步機制方面。話簡短來講: 假設(shè)庫能夠更為幫你去處理并行化方面, 那么你就不存在再借著自己動用手腳, 手去編寫并行代碼的這種必要性。進階組合與協(xié)同如果你想將性能提升到更高的層次可以將這些庫進行組合當, 你的技術(shù)棧, 能夠, 與性能, 協(xié)同, 而不是, 對抗性能時, 多線程, 就會, 徹底, 淪為, 最后的, 選擇, 而不是, 必需品。九、總結(jié)速度與簡潔性并行不悖曾經(jīng), 于往昔歲月之際, 若去追尋性能, 常常就意味著要去接納復雜性, 諸如線程, 還有鎖、隊列這一類, 以及形形色色會讓人倍感頭疼的諸多問題。眼下恰當?shù)墓ぞ吣軌蛴谀缓筇婺氵_成全部并發(fā)事項。諸如PyPy、Numba以及Ray等這六個庫證明確實可以一并兼具優(yōu)雅的代碼構(gòu)建以及宛如閃電般快捷的速度, 與此同時也無需去犧牲開發(fā)者的那種幸福感。那么, 下次哪時你發(fā)覺腳本運轉(zhuǎn)遲緩, 請莫再慣常性抓取線程咯, 而是要去覓尋出一個更為聰慧、更具優(yōu)化性的庫, 。并非是同時開展諸多事情, 方才稱得上具備真正的速度, 而是唯獨把一件事情, 做到極為卓越出色的那種程度, 稱得上真正有速度, 這才是關(guān)鍵所在。