作者:?ukasz Langa
創(chuàng)新互聯(lián)是專(zhuān)業(yè)的咸豐網(wǎng)站建設(shè)公司,咸豐接單;提供成都做網(wǎng)站、成都網(wǎng)站制作,網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專(zhuān)業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行咸豐網(wǎng)站開(kāi)發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專(zhuān)業(yè)做搜索引擎喜愛(ài)的網(wǎng)站,專(zhuān)業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來(lái)合作!
譯者:豌豆花下貓,來(lái)源:Python貓
原文:https://lukasz.langa.pl/5d044f91-49c1-4170-aed1-62b6763e6ad0
在一年一度的 Python 核心開(kāi)發(fā)者 sprint 會(huì)議期間,我們與 Sam Gross 舉行了一次會(huì)議,他是 nogil 的作者。nogil 是 Python 3.9 的分叉版本,移除了 GIL。這是一份非正式的會(huì)議紀(jì)要。
Sam 的工作證明了以他的方式刪除 GIL 是可行的,即生成的 Python 解釋器的性能良好,并且可以隨著 CPU 內(nèi)核的增加而擴(kuò)展。為了最終達(dá)到正面的效果,還需要有其它看似無(wú)關(guān)的解釋器工作。
目前還不可能將 Sam 的更改合并到 CPython,因?yàn)樗母氖轻槍?duì) 3.9 分支進(jìn)行的,便于用戶(hù)拿當(dāng)前 pip 可安裝的庫(kù)和 C 擴(kuò)展對(duì) nogil 解釋器進(jìn)行測(cè)試。如果要合并 nogil,就不得不基于 main 分支進(jìn)行更改(目前 main 分支已規(guī)劃為 3.11)。
不要指望 Python 3.11 會(huì)移除 GIL。 將 Sam 的工作合并到 CPython 本身將是一個(gè)艱苦的過(guò)程,但這僅僅是所需的一部分:在 CPython 移除 GIL 之前,需要為社區(qū)制定一個(gè)良好的向后兼容的遷移計(jì)劃。這些都還沒(méi)有計(jì)劃好,所以我們認(rèn)為時(shí)機(jī)還沒(méi)到。
有些人在談?wù)撊绱司薮蟮淖兓瘯r(shí)提到了 Python 4。核心開(kāi)發(fā)人員當(dāng)前沒(méi)有計(jì)劃發(fā)布 Python 4,事實(shí)上恰恰相反:我們正積極地避免發(fā)布 Python 4,因?yàn)?Python 2 到 3 的轉(zhuǎn)換對(duì)社區(qū)來(lái)說(shuō)已經(jīng)足夠困難了。現(xiàn)在考慮或者擔(dān)心 Python 4,肯定還為時(shí)過(guò)早。
Sam 發(fā)布了他的代碼,同時(shí)還有一篇詳細(xì)的文章,解釋了該項(xiàng)目的動(dòng)機(jī)和設(shè)計(jì)。
nogil 代碼地址:https://github.com/colesbury/nogil
他的設(shè)計(jì)可以總結(jié)為:
pymalloc
替換成mimalloc
,對(duì)字典和其它集合對(duì)象采用無(wú)鎖讀寫(xiě),同時(shí)提升效率(堆內(nèi)存布局允許在不維護(hù)顯式列表的情況下找到 GC 跟蹤的對(duì)象)PyEval_ReleaseThread
,相當(dāng)于在當(dāng)前 Python 中釋放 GIL);mimalloc
, GC 跟蹤的對(duì)象都保存在一個(gè)單獨(dú)的輕量級(jí)的堆中;Sam 的設(shè)計(jì)文檔包含了這些設(shè)計(jì)元素的細(xì)節(jié),包含線(xiàn)程狀態(tài)與 GIL API 的信息,以及解釋器和字節(jié)碼的其它修改(用帶有累加器的寄存器 VM 替換堆棧VM;通過(guò)避免創(chuàng)建 C 語(yǔ)言的棧幀來(lái)優(yōu)化函數(shù)調(diào)用;ceval.c 的其它變更;標(biāo)簽指針的使用;LOAD_ATTR、LOAD_METHOD、 LOAD_GLOBAL 操作碼的線(xiàn)程安全的元數(shù)據(jù);等等)。我建議你完整地閱讀它。
Python貓注:上文出現(xiàn)的“stop-the-world”,有時(shí)縮寫(xiě)成“STW”,這是多數(shù)垃圾回收器的工作機(jī)制,表示在垃圾回收器工作時(shí),其它線(xiàn)程全部暫時(shí)掛起,從而保證引用對(duì)象的準(zhǔn)確更新,其缺點(diǎn)是對(duì)程序性能有所影響;“MRO”是“method resolution order”的縮寫(xiě),即“類(lèi)方法解析順序”,表示在所有基類(lèi)中搜索成員方法時(shí)的次序。
在 pyperformance 基準(zhǔn)測(cè)試套上,作為概念驗(yàn)證的 nogil 解釋器比 3.9 快 10%。據(jù)估計(jì),在解釋器的全部修改中,移除 GIL 會(huì)導(dǎo)致性能變慢 9%,主要是因?yàn)橛衅?jiàn)的引用計(jì)數(shù)和延遲引用計(jì)數(shù)。換句話(huà)說(shuō),Python 3.9 加上 nogil 的所有更改,但不移除 GIL 本身,可以快 19%。然而,這樣并不能解決多核的可伸縮性問(wèn)題。
順便說(shuō)一下,nogil 的一些更改,比如將 C 調(diào)用棧與 Python 調(diào)用棧解耦,已經(jīng)在 Python 3.11 中實(shí)現(xiàn)了。事實(shí)上,我們有針對(duì)當(dāng)前 main 分支的初步的基準(zhǔn)測(cè)試 ,結(jié)果表明在單線(xiàn)程的性能上,Python 3.11 比 nogil 快 16%。
需要有更多的基準(zhǔn)測(cè)試,特別是使用 Larry Hastings 在對(duì) Gilectomy 進(jìn)行測(cè)試時(shí)使用的基準(zhǔn)測(cè)試(當(dāng)時(shí)基于 Python 3.5,后來(lái)移植到 3.6 alpha 1)。
Python貓注:gilectomy 是由 GIL ectomy 兩個(gè)單詞組合而成,ectomy 是一個(gè)醫(yī)學(xué)上的術(shù)語(yǔ)“切除術(shù)”,可見(jiàn)這個(gè)項(xiàng)目的用意跟 nogil 是一樣的!這是 5-6 年前的項(xiàng)目,作者曾在 PyCon 大會(huì)上做過(guò)幾次分享。但這個(gè)項(xiàng)目反而導(dǎo)致 Python 總體性能下降了,最后無(wú)疾而終。
gilectomy 項(xiàng)目作者在 PyCon 上的分享:
2015年分享:https://www.youtube.com/watch?v=KVKufdTphKs
2016年分享:https://www.youtube.com/watch?v=P3AyI_u66Bw
2017年分享:https://www.youtube.com/watch?v=pLqv11ScGsQ
Sam 提醒我們,一個(gè)用戶(hù)程序在無(wú) GIL 的 Python 上的伸縮性實(shí)際上取決于最終的代碼。如果不進(jìn)行測(cè)試,就不可能預(yù)測(cè)代碼在沒(méi)有 GIL 的情況下表現(xiàn)如何。因此,如果提供一個(gè)單一的數(shù)字來(lái)說(shuō)明無(wú) GIL 的 Python 速度會(huì)提升 x 倍,這是不負(fù)責(zé)任的。
為了清晰易懂,這里的問(wèn)題基于會(huì)議上的內(nèi)容進(jìn)行了重新排序。答案是由 Sam 的回答轉(zhuǎn)述而來(lái)的,并得到了他閱讀草稿后的認(rèn)可。要注意的是,核心團(tuán)隊(duì)的成員可能對(duì)其中一些主題有其它觀點(diǎn)。
目前的代碼庫(kù)已經(jīng)證明了它在技術(shù)上的可行性。它可以運(yùn)行,而且比普通的 CPython 解釋器和 Gilectomy 項(xiàng)目更具有可伸縮性和好性能。我在該項(xiàng)目中投入了將近兩年的全職工作。
這完全取決于社區(qū)對(duì) C 擴(kuò)展程序的改造程度,以確保它們不會(huì)導(dǎo)致解釋器徹底崩潰。然后,剩下的長(zhǎng)尾就是社區(qū)要以一種既正確又可擴(kuò)展的方式在應(yīng)用程序中采用自由線(xiàn)程。這兩個(gè)是最大的挑戰(zhàn),但我們必須樂(lè)觀應(yīng)對(duì)。
Sam 目前正在重構(gòu)他的工作,最初是基于 3.9.0a3,將匹配 3.9.7 最終版本。這項(xiàng)工作的一部分是將 commit 重構(gòu)為邏輯單元,以便更好地說(shuō)明哪些內(nèi)容需要更改(哪些地方改了,以及為什么要改)。
目前還不計(jì)劃把這項(xiàng)工作移到 main 分支(未來(lái)的 3.11),因?yàn)檫@個(gè)分支太不穩(wěn)定了。相比之下,3.9 有大量已發(fā)布的可通過(guò) pip 安裝的庫(kù)和 C 擴(kuò)展,可用于測(cè)試。這使得 Sam 能夠評(píng)估該項(xiàng)目與真實(shí)世界的第三方代碼的行為。基于 main 的修改將花費(fèi)不少時(shí)間,而這些時(shí)間本可以花在改進(jìn)無(wú) GIL 的解釋器上,所以,現(xiàn)在就基于主分支的話(huà),還為時(shí)過(guò)早。
將工作進(jìn)行分割然后再合并是可行的,但必須記住,許多更新需要在串聯(lián)起來(lái)時(shí),性能才會(huì)提升。單獨(dú)而言,它們會(huì)導(dǎo)致(暫時(shí)的?)性能下降。
核心開(kāi)發(fā)者注:我們現(xiàn)在不能合并對(duì) 3.9 分支所做的更改。在項(xiàng)目的這個(gè)階段使用 3.9 是有意義的,但關(guān)鍵的是要將它分割成可消費(fèi)的數(shù)據(jù)塊,然后一個(gè)一個(gè)地合并到 main 分支中。一塊一塊地做,很有可能會(huì)損害性能,但這是唯一現(xiàn)實(shí)的集成途徑。
VM 使用延遲/永生的引用計(jì)數(shù)。可以將其轉(zhuǎn)換為只使用經(jīng)典的引用計(jì)數(shù),但最終結(jié)果的效率還不清楚(例如,出于性能考慮,堆棧上的所有對(duì)象都使用了延遲引用計(jì)數(shù))。
雖然新的 VM 只提高了性能,而不是準(zhǔn)確性,但它也提高了可伸縮性,使得無(wú) GIL 的 Python 可以充分利用 CPU 內(nèi)核而不發(fā)生爭(zhēng)用。因此要使用 3.11 解釋器也是可行的,但最好保留一些寄存器 VM 的設(shè)計(jì)思想,這對(duì)可伸縮性和線(xiàn)程安全很重要。這需要做大量的工作。但是將寄存器 VM 更新成跟 main 分支一樣(以及修復(fù)遺留的 bug),也需要大量的工作。這兩種選擇都是可行的。
這需要花時(shí)間。目標(biāo)是漸進(jìn)式采納,最終推廣至大多數(shù) C 擴(kuò)展。GIL 可以作為解釋器啟動(dòng)時(shí)的一個(gè)選項(xiàng)。如果沒(méi)有啟用 GIL,并且 C 擴(kuò)展不支持新的操作模式,可能就要產(chǎn)生告警或者不讓其導(dǎo)入。Python 社區(qū)不得不適配 C 擴(kuò)展,讓它們適應(yīng)無(wú) GIL 的模式。
作為概念驗(yàn)證的 nogil 項(xiàng)目,默認(rèn)使用無(wú) GIL 模式,并接受任何 C 擴(kuò)展。如果它被 CPython 采用了,那么在開(kāi)始時(shí)默認(rèn)應(yīng)該啟用 GIL(要求在啟動(dòng) Python 時(shí)使用 -X nogil
禁用 GIL),以便讓第三方庫(kù)做適配。然后,在發(fā)布幾個(gè)版本后,默認(rèn)值再切換成無(wú) GIL 的模式。
雖然要移植全部東西并不容易(并行是很難的),但在多數(shù)情況下,移植并不會(huì)很難,特別是對(duì)于封裝外部庫(kù)的 C 擴(kuò)展來(lái)說(shuō)。
核心開(kāi)發(fā)者注:有大量的“暗物質(zhì)” Python 代碼(和 C 擴(kuò)展)不是開(kāi)源的。我們需要小心不去破壞它們,因?yàn)樗鼈兊挠脩?hù)可能無(wú)法做出所需的更改,或者向上游報(bào)告問(wèn)題給我們。特別地,有些 C 擴(kuò)展使用 GIL 來(lái)保護(hù)它們自己的內(nèi)部狀態(tài)。這是一個(gè)很大的擔(dān)憂(yōu),可能是采用無(wú) GIL Python 的一個(gè)很大的障礙。
很多人也提過(guò),這可能是一個(gè)好主意,但我不完全清楚這意味著什么。選擇無(wú) GIL 模式并不能保證沒(méi)有 bug。相反,在默認(rèn)情況下,我們運(yùn)行所有的擴(kuò)展(現(xiàn)在的 nogil 就是這么做的)。不兼容的擴(kuò)展可以使用 PyInit 模塊的代碼,主動(dòng)地詢(xún)問(wèn)解釋器是否啟用了 GIL,如果不兼容的話(huà),就在導(dǎo)入時(shí)產(chǎn)生警告甚至異常。
理想的結(jié)局是 CPython 不再有 GIL,句號(hào)。然而,預(yù)計(jì)將有一個(gè)漫長(zhǎng)的社區(qū)適應(yīng)期。我們希望避免從 Python2 到 Python3 過(guò)渡時(shí)的斷裂。準(zhǔn)確地說(shuō),我們希望過(guò)渡得越平滑越好,即使這意味著需要延展更長(zhǎng)的時(shí)間。
目前我們還不確定。理想的結(jié)局是只存在一個(gè)無(wú) GIL 的 Python,但尚不清楚這能否實(shí)現(xiàn)。
是的,測(cè)試矩陣需要加倍。然而,測(cè)試無(wú) GIL 版本可能是判斷經(jīng)典的 GIL 版本是否有效的一個(gè)很好的預(yù)測(cè)器。有必要偶爾(每晚?)運(yùn)行啟用了 GIL 的測(cè)試。
核心開(kāi)發(fā)者注:如果不做測(cè)試,代碼將加速退化。在 CPython 中,由于需要運(yùn)行時(shí)間(例如測(cè)試引用泄漏時(shí)),我們不會(huì)在每次更改時(shí)都運(yùn)行所有測(cè)試,但如果有更改導(dǎo)致每日測(cè)試失敗,我們會(huì)立即回退更改,因?yàn)樵谝呀?jīng)失敗的構(gòu)建點(diǎn)之后,很可能會(huì)出現(xiàn)其它的回歸問(wèn)題。
Python貓注:給大家科普一下這個(gè)問(wèn)題的背景,PEP-554 提議實(shí)現(xiàn)多解釋器來(lái)解決 GIL 的問(wèn)題。這是在 2017 年提出的,受到挺多關(guān)注。在 2019 年時(shí),我曾翻譯過(guò)《Has the Python GIL been slain?》介紹它。但是,目前該提案依然是草稿狀態(tài),具體的開(kāi)發(fā)情況不甚明朗。
跟無(wú) GIL 提案相比,這既是互補(bǔ)的,又是相互競(jìng)爭(zhēng)的。在無(wú) GIL 解釋器中也可以支持副解釋器。
目前還不清楚多解釋器方案能否實(shí)現(xiàn)。有了 nogil,就不需要擔(dān)心跨線(xiàn)程共享對(duì)象,也不需要擔(dān)心 C 擴(kuò)展的兼容性,因?yàn)橛辛硕嘟忉屍鳎蜎](méi)有任何狀態(tài)是真正全局的,因此需要特別地隔離。對(duì)于可變對(duì)象,在多解釋器之間傳遞時(shí),需要某種形式的序列化/反序列化。對(duì)于不可變對(duì)象,解釋器可能會(huì)添加特殊的支持,但如果它們不是已知的不可變的內(nèi)置類(lèi)型,用戶(hù)代碼就需要適配這些對(duì)象。這是從 PyTorch 的相關(guān)工作中得到的啟發(fā),它使用了某種形式的多解釋器。
由于我最感興趣的用例實(shí)際上是科學(xué)數(shù)據(jù)(PyTorch 訓(xùn)練工作流),直接而有效地共享數(shù)據(jù)的能力對(duì)多線(xiàn)程性能至關(guān)重要。如果采用多解釋器,這種共享只能在 C 擴(kuò)展級(jí)別上開(kāi)啟,與無(wú) GIL 的 Python 相比,將導(dǎo)致更多使用 C/C++ 代碼。
nogil 是一個(gè)開(kāi)發(fā)中的項(xiàng)目。由于字典和列表在解釋器的內(nèi)部運(yùn)作中很普遍,所以它們的開(kāi)發(fā)最多。同樣地,隊(duì)列的開(kāi)發(fā)已經(jīng)完成,但其它類(lèi)型還沒(méi)有。集合是下一個(gè)要覆蓋的重要內(nèi)容。
隊(duì)列非常重要,因?yàn)樗?code>concurrent.futures 和asyncio
用于并發(fā)線(xiàn)程之間的通信。隊(duì)列比字典和列表簡(jiǎn)單,它使用細(xì)粒度的鎖而不是無(wú)鎖讀取。其它的對(duì)象很可能需要組合使用。
這項(xiàng)工作很棘手,因?yàn)樵讷@取和釋放鎖時(shí)需要小心,例如 Py_DECREFs 是可重入的。還可以考慮使用更“粗粒度”的鎖,但當(dāng)然了,這些鎖都有死鎖的風(fēng)險(xiǎn)。
mimalloc 不僅僅是用于線(xiàn)程安全。它對(duì)于啟用字典的無(wú)鎖讀取是必要的,還支持高效的 GC 追蹤。
mimalloc 的維護(hù)者對(duì)顯式地支持 CPython 很感興趣,并且樂(lè)意為實(shí)現(xiàn)這一點(diǎn)進(jìn)行必要的更改。
其它實(shí)現(xiàn)的 malloc 據(jù)說(shuō)也穩(wěn)定支持 CPython:在 Facebook 中使用的jemalloc
,在谷歌中使用tcmalloc
,盡管集成得較少,更像是默認(rèn)分配器的簡(jiǎn)單替換。(Python貓注:前文提到的 mimalloc 是微軟的)
核心開(kāi)發(fā)者注:Christian Heimes 和 Pablo Galindo Salgado 正在評(píng)估 CPython 使用 mimalloc。早期測(cè)試在平均上(幾何平均數(shù))沒(méi)有性能衰退,大多數(shù)基準(zhǔn)測(cè)試做得更好,少數(shù)基準(zhǔn)測(cè)試做得稍微差一些。還有一些待評(píng)估的問(wèn)題:
在頂層設(shè)計(jì)上,兩個(gè)項(xiàng)目是相似的:延遲引用計(jì)數(shù),細(xì)粒度鎖,關(guān)于返回借用的引用的挑戰(zhàn)。沒(méi)有復(fù)用 Gilectomy 的代碼。
切換到基于寄存器的編譯器和其它優(yōu)化,比如由 mimalloc 提供的無(wú)鎖的字典讀取,以及使用延遲引用計(jì)數(shù)來(lái)避免爭(zhēng)用,對(duì) nogil 的擴(kuò)展性和性能都至關(guān)重要。而且,在某些情況下,Python 本身變得更快了。例如, Python 3.9 中的函數(shù)調(diào)用比 Python 3.5 的要快得多。
讓它支持?jǐn)U展,肯定比預(yù)期要花更多的工作。
顧名思義,GIL 就是一個(gè)全局鎖。為了保護(hù)任意一段共享數(shù)據(jù),它需要在所有線(xiàn)程上開(kāi)啟,包括不兼容的擴(kuò)展所處的線(xiàn)程。
在已經(jīng)運(yùn)行的進(jìn)程中,將無(wú) GIL 的解釋器切換為使用 GIL 的解釋器是很棘手的(反之亦然)。最好的做法是在啟動(dòng)時(shí)選擇:要么在進(jìn)程中啟用 GIL,要么不啟用。如果 C 擴(kuò)展沒(méi)有標(biāo)記為兼容,就引發(fā)警告或無(wú)法導(dǎo)入。
或者,當(dāng)訪(fǎng)問(wèn) C 擴(kuò)展時(shí),也可以“stop the world”,但這與移除 GIL 而所想達(dá)成的目的不符。
核心開(kāi)發(fā)者注:到目前為止,還有其它的想法需要深入探討。有種想法是將 GIL 轉(zhuǎn)換為“單寫(xiě)多讀”鎖。在這種情況下,無(wú) GIL 的模式將獲取“多讀”鎖,也就是說(shuō),不會(huì)阻塞其它新代碼做同樣的事情。而歷史遺留的代碼將獲得一個(gè)“單寫(xiě)”鎖,阻塞其它所有線(xiàn)程執(zhí)行,直到鎖釋放。這種設(shè)計(jì)需要保留獲取/釋放 GIL 的 api,nogil 已經(jīng)這樣做了,為了告知 GC 一個(gè)線(xiàn)程被阻塞在 I/O 上。
如果擔(dān)心的是狀態(tài)被其它線(xiàn)程訪(fǎng)問(wèn),則需要鎖定每一次訪(fǎng)問(wèn)。這在裝飾器層面上不是特別可行。正如之前說(shuō)過(guò),條件性地為不安全的代碼開(kāi)啟 GIL 是很難實(shí)現(xiàn)的。
不清楚。對(duì)于 C API 擴(kuò)展,至少有一種好的設(shè)計(jì)模式:它們通常有類(lèi)似的結(jié)構(gòu),并在單個(gè)結(jié)構(gòu)中保持共享狀態(tài)。目前,Pybind11 看起來(lái)與這個(gè)模式距離最遠(yuǎn),因此用它編寫(xiě)的 C 擴(kuò)展可能需要進(jìn)行大量更改。
許多復(fù)雜的 C 擴(kuò)展已經(jīng)不得不處理鎖和多線(xiàn)程,因?yàn)樗鼈兊哪康氖潜M可能多地釋放 GIL,比如 numpy。所以,也許令人驚訝的是,那些項(xiàng)目可能更容易遷移。
在這次會(huì)議之后,核心開(kāi)發(fā)者們討論了將 nogil 納入主項(xiàng)目的可行性,以及這對(duì)社區(qū)意味著什么。毫無(wú)疑問(wèn),這種程度的改變必須非常小心。
在作出決定之前,我們覺(jué)得先引入它的一些代碼更為可行。特別地,mimalloc 看起來(lái)很有趣,已經(jīng)有一個(gè) open 的 pull 請(qǐng)求了(https://github.com/python/cpython/pull/),旨在探索引入它。在那里可以找到基準(zhǔn)測(cè)試的鏈接。
在個(gè)人層面上,我們對(duì) Sam 所做的工作印象深刻,并邀請(qǐng)他加入 CPython 項(xiàng)目。我很高興地告訴大家,他對(duì)此很感興趣,為了幫助他成為一名核心開(kāi)發(fā)者,我將為他提供指導(dǎo)。Guido 和 Neil Schemenauer 將幫我檢視我不熟悉的解釋器部分的代碼。