Python3 cpython优化 实现解释器并行
本文介紹了對(duì)cpython解釋器的并行優(yōu)化,使其支持真正的多解釋器并行執(zhí)行的解決方案。
作者:字節(jié)跳動(dòng)終端技術(shù)——謝俊逸
背景
在業(yè)務(wù)場(chǎng)景中,我們通過(guò)cpython執(zhí)行算法包,由于cpython的實(shí)現(xiàn),在一個(gè)進(jìn)程內(nèi),無(wú)法利用CPU的多個(gè)核心去同時(shí)執(zhí)行算法包。對(duì)此,我們決定優(yōu)化cpython,目標(biāo)是讓cpython高完成度的支持并行,大幅度的提高單個(gè)進(jìn)程內(nèi)Python算法包的執(zhí)行效率。
在2020年,我們完成了對(duì)cpython的并行執(zhí)行改造,是目前業(yè)界首個(gè)cpython3的高完成度同時(shí)兼容Python C API的并行實(shí)現(xiàn)。
-
性能
- 單線程性能劣化7.7%
- 多線程基本無(wú)鎖搶占,多開(kāi)一個(gè)線程減少44%的執(zhí)行時(shí)間。
- 并行執(zhí)行對(duì)總執(zhí)行時(shí)間有大幅度的優(yōu)化
-
通過(guò)了cpython的單元測(cè)試
-
在線上已經(jīng)全量使用
cpython痛, GIL
cpython是python官方的解釋器實(shí)現(xiàn)。在cpython中,GIL,用于保護(hù)對(duì)Python對(duì)象的訪問(wèn),從而防止多個(gè)線程同時(shí)執(zhí)行Python字節(jié)碼。GIL防止出現(xiàn)競(jìng)爭(zhēng)情況并確保線程安全。 因?yàn)镚IL的存在,cpython 是無(wú)法真正的并行執(zhí)行python字節(jié)碼的. GIL雖然限制了python的并行,但是因?yàn)閏python的代碼沒(méi)有考慮到并行執(zhí)行的場(chǎng)景,充滿著各種各樣的共享變量,改動(dòng)復(fù)雜度太高,官方一直沒(méi)有移除GIL。
挑戰(zhàn)
在Python開(kāi)源的20年里,Python 因?yàn)镚IL(全局鎖)不能并行。目前主流實(shí)現(xiàn)Python并行的兩種技術(shù)路線,但是一直沒(méi)有高完成度的解決方案(高性能,兼容所有開(kāi)源feature, API穩(wěn)定)。主要是因?yàn)?#xff1a;
Back in the days of Python 1.5, Greg Stein actually implemented a comprehensive patch set (the “free threading” patches) that removed the GIL and replaced it with fine-grained locking. Unfortunately, even on Windows (where locks are very efficient) this ran ordinary Python code about twice as slow as the interpreter using the GIL. On Linux the performance loss was even worse because pthread locks aren’t as efficient.
It has been suggested that the GIL should be a per-interpreter-state lock rather than truly global; interpreters then wouldn’t be able to share objects. Unfortunately, this isn’t likely to happen either. It would be a tremendous amount of work, because many object implementations currently have global state. For example, small integers and short strings are cached; these caches would have to be moved to the interpreter state. Other object types have their own free list; these free lists would have to be moved to the interpreter state. And so on.
這個(gè)思路開(kāi)源有一個(gè)項(xiàng)目在做 multi-core-python,但是目前已經(jīng)擱置了。目前只能運(yùn)行非常簡(jiǎn)單的算術(shù)運(yùn)算的demo。對(duì)Type和許多模塊的并行執(zhí)行問(wèn)題并沒(méi)有處理,無(wú)法在實(shí)際場(chǎng)景中使用。
新架構(gòu)-多解釋器架構(gòu)
為了實(shí)現(xiàn)最佳的執(zhí)行性能,我們參考multi-core-python,在cpython3.10實(shí)現(xiàn)了一個(gè)高完成度的并行實(shí)現(xiàn)。
- 從全局解釋器狀態(tài) 轉(zhuǎn)換為 每個(gè)解釋器結(jié)構(gòu)持有自己的運(yùn)行狀態(tài)(獨(dú)立的GIL,各種執(zhí)行狀態(tài))。
- 支持并行,解釋器狀態(tài)隔離,并行執(zhí)行性能不受解釋器個(gè)數(shù)的影響(解釋器間基本沒(méi)有鎖相互搶占)
- 通過(guò)線程的Thread Specific Data獲取Python解釋器狀態(tài)。
在這套新架構(gòu)下,Python的解釋器相互隔離,不共享GIL,可以并行執(zhí)行。充分利用現(xiàn)代CPU的多核性能。大大減少了業(yè)務(wù)算法代碼的執(zhí)行時(shí)間。
共享變量的隔離
解釋器執(zhí)行中使用了很多共享的變量,他們普遍以全局變量的形式存在.多個(gè)解釋器運(yùn)行時(shí),會(huì)同時(shí)對(duì)這些共享變量進(jìn)行讀寫操作,線程不安全。
cpython內(nèi)部的主要共享變量:3.10待處理的共享變量。大概有1000個(gè)…需要處理,工作量非常之大。
-
free lists
- MemoryError
- asynchronous generator
- context
- dict
- float
- frame
- list
- slice
-
singletons
- small integer ([-5; 256] range)
- empty bytes string singleton
- empty Unicode string singleton
- empty tuple singleton
- single byte character (b’\x00’ to b’\xFF’)
- single Unicode character (U+0000-U+00FF range)
-
cache
- slide cache
- method cache
- bigint cache
- …
-
interned strings
-
PyUnicode_FromId static strings
-
…
如何讓每個(gè)解釋器獨(dú)有這些變量呢?
cpython是c語(yǔ)言實(shí)現(xiàn)的,在c中,我們一般會(huì)通過(guò) 參數(shù)中傳遞 interpreter_state 結(jié)構(gòu)體指針來(lái)保存屬于一個(gè)解釋器的成員變量。這種改法也是性能上最好的改法。但是如果這樣改,那么所有使用interpreter_state的函數(shù)都需要修改函數(shù)簽名。從工程角度上是幾乎無(wú)法實(shí)現(xiàn)的。
只能換種方法,我們可以將interpreter_state存放到thread specific data中。interpreter執(zhí)行時(shí),通過(guò)thread specific key獲取到 interpreter_state.這樣就可以通過(guò)thread specific的API,獲取到執(zhí)行狀態(tài),并且不用修改函數(shù)的簽名。
static inline PyInterpreterState* _PyInterpreterState_GET(void) {PyThreadState *tstate = _PyThreadState_GET(); #ifdef Py_DEBUG_Py_EnsureTstateNotNULL(tstate); #endifreturn tstate->interp; }共享變量變?yōu)榻忉屍鲉为?dú)持有 我們將所有的共享變量存放到 interpreter_state里。
/* Small integers are preallocated in this array so that theycan be shared.The integers that are preallocated are those in the range-_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (not inclusive).*/PyLongObject* small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];struct _Py_bytes_state bytes;struct _Py_unicode_state unicode;struct _Py_float_state float_state;/* Using a cache is very effective since typically only a single slice iscreated and then deleted again. */PySliceObject *slice_cache;struct _Py_tuple_state tuple;struct _Py_list_state list;struct _Py_dict_state dict_state;struct _Py_frame_state frame;struct _Py_async_gen_state async_gen;struct _Py_context_state context;struct _Py_exc_state exc_state;struct ast_state ast;struct type_cache type_cache; #ifndef PY_NO_SHORT_FLOAT_REPRstruct _PyDtoa_Bigint *dtoa_freelist[_PyDtoa_Kmax + 1]; #endif通過(guò) _PyInterpreterState_GET 快速訪問(wèn)。 例如
/* Get Bigint freelist from interpreter */ static Bigint ** get_freelist(void) {PyInterpreterState *interp = _PyInterpreterState_GET();return interp->dtoa_freelist; }注意,將全局變量改為thread specific data是有性能影響的,不過(guò)只要控制該API調(diào)用的次數(shù),性能影響還是可以接受的。 我們?cè)赾python3.10已有改動(dòng)的的基礎(chǔ)上,解決了各種各樣的共享變量問(wèn)題,3.10待處理的共享變量
Type變量共享的處理,API兼容性及解決方案
目前cpython3.x 暴露了PyType_xxx 類型變量在API中。這些全局類型變量被第三方擴(kuò)展代碼以&PyType_xxx的方式引用。如果將Type隔離到子解釋器中,勢(shì)必造成不兼容的問(wèn)題。這也是官方改動(dòng)停滯的原因,這個(gè)問(wèn)題無(wú)法以合理改動(dòng)的方式出現(xiàn)在python3中。只能等到python4修改API之后改掉。
我們通過(guò)另外一種方式快速的改掉了這個(gè)問(wèn)題。
Type是共享變量會(huì)導(dǎo)致以下的問(wèn)題
改法:
immortal type object.
使用頻率低的不安全處加鎖。
高頻使用的場(chǎng)景,使用的成員變量設(shè)置為immortal object.
這樣會(huì)導(dǎo)致Type和成員變量會(huì)內(nèi)存泄漏。不過(guò)由于cpython有module的緩存機(jī)制,不清理緩存時(shí),便沒(méi)有問(wèn)題。
pymalloc內(nèi)存池共享處理
我們使用了mimalloc替代pymalloc內(nèi)存池,在優(yōu)化1%-2%性能的同時(shí),也不需要額外處理pymalloc。
subinterperter 能力補(bǔ)全
官方master最新代碼 subinterpreter 模塊只提供了interp_run_string可以執(zhí)行code_string. 出于體積和安全方面的考慮,我們已經(jīng)刪除了python動(dòng)態(tài)執(zhí)行code_string的功能。 我們給subinterpreter模塊添加了兩個(gè)額外的能力
subinterpreter 執(zhí)行模型
python中,我們執(zhí)行代碼默認(rèn)運(yùn)行的是main interpreter, 我們也可以創(chuàng)建的sub interpreter執(zhí)行代碼,
interp = _xxsubinterpreters.create() result = _xxsubinterpreters.interp_call_function(*args, **kwargs)這里值得注意的是,我們是在 main interpreter 創(chuàng)建 sub interpreter, 隨后在sub interpreter 執(zhí)行,最后把結(jié)果返回到main interpreter. 這里看似簡(jiǎn)單,但是做了很多事情。
這里有兩個(gè)復(fù)雜的地方:
interpreter state 狀態(tài)的切換
interp = _xxsubinterpreters.create() result = _xxsubinterpreters.interp_call_function(*args, **kwargs)我們可以分解為
# Running In thread 11: # main interpreter: # 現(xiàn)在 thread specific 設(shè)置的 interpreter state 是 main interpreter的 do some things ... create subinterpreter ... interp_call_function ... # thread specific 設(shè)置 interpreter state 為 sub interpreter state # sub interpreter: do some thins ... call function ... get result ... # 現(xiàn)在 thread specific 設(shè)置 interpreter state 為 main interpreter state get return result ...interpreter 數(shù)據(jù)的傳遞
因?yàn)槲覀兘忉屍鞯膱?zhí)行狀態(tài)是隔離的,在main interpreter 中創(chuàng)建的 Python Object是無(wú)法在 sub interpreter 使用的. 我們需要:
interpreter 狀態(tài)的切換 & 數(shù)據(jù)的傳遞 的實(shí)現(xiàn)可以參考以下示例 …
static PyObject * _call_function_in_interpreter(PyObject *self, PyInterpreterState *interp, _sharedns *args_shared, _sharedns *kwargs_shared) {PyObject *result = NULL;PyObject *exctype = NULL;PyObject *excval = NULL;PyObject *tb = NULL;_sharedns *result_shread = _sharedns_new(1);#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS// Switch to interpreter.PyThreadState *new_tstate = PyInterpreterState_ThreadHead(interp);PyThreadState *save1 = PyEval_SaveThread();(void)PyThreadState_Swap(new_tstate); #else// Switch to interpreter.PyThreadState *save_tstate = NULL;if (interp != PyInterpreterState_Get()) {// XXX Using the head thread isn't strictly correct.PyThreadState *tstate = PyInterpreterState_ThreadHead(interp);// XXX Possible GILState issues?save_tstate = PyThreadState_Swap(tstate);} #endifPyObject *module_name = _PyCrossInterpreterData_NewObject(&args_shared->items[0].data);PyObject *function_name = _PyCrossInterpreterData_NewObject(&args_shared->items[1].data);...PyObject *module = PyImport_ImportModule(PyUnicode_AsUTF8(module_name));PyObject *function = PyObject_GetAttr(module, function_name);result = PyObject_Call(function, args, kwargs);...#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS// Switch back.PyEval_RestoreThread(save1); #else// Switch back.if (save_tstate != NULL) {PyThreadState_Swap(save_tstate);} #endifif (result) {result = _PyCrossInterpreterData_NewObject(&result_shread->items[0].data);_sharedns_free(result_shread);}return result; }實(shí)現(xiàn)子解釋器池
我們已經(jīng)實(shí)現(xiàn)了內(nèi)部的隔離執(zhí)行環(huán)境,但是這是API比較低級(jí),需要封裝一些高度抽象的API,提高子解釋器并行的易用能力。
interp = _xxsubinterpreters.create() result = _xxsubinterpreters.interp_call_function(*args, **kwargs)這里我們參考了,python concurrent庫(kù)提供的 thread pool, process pool, futures的實(shí)現(xiàn),自己實(shí)現(xiàn)了 subinterpreter pool. 通過(guò)concurrent.futures 模塊提供異步執(zhí)行回調(diào)高層接口。
executer = concurrent.futures.SubInterpreterPoolExecutor(max_workers) future = executer.submit(_xxsubinterpreters.call_function, module_name, func_name, *args, **kwargs) future.context = context future.add_done_callback(executeDoneCallBack)我們內(nèi)部是這樣實(shí)現(xiàn)的: 繼承 concurrent 提供的 Executor 基類
class SubInterpreterPoolExecutor(_base.Executor):SubInterpreterPool 初始化時(shí)創(chuàng)建線程,并且每個(gè)線程創(chuàng)建一個(gè) sub interpreter
interp = _xxsubinterpreters.create() t = threading.Thread(name=thread_name, target=_worker,args=(interp, weakref.ref(self, weakref_cb),self._work_queue,self._initializer,self._initargs))線程 worker 接收參數(shù),并使用 interp 執(zhí)行
result = self.fn(self.interp ,*self.args, **self.kwargs)實(shí)現(xiàn)外部調(diào)度模塊
針對(duì)sub interpreter的改動(dòng)較大,存在兩個(gè)隱患
我們希望能統(tǒng)一對(duì)外的接口,讓使用者不需要關(guān)注這些細(xì)節(jié),我們自動(dòng)的切換調(diào)用方式。自動(dòng)選擇在主解釋器使用(兼容性好,穩(wěn)定)還是子解釋器(支持并行,性能佳)
我們提供了C和python的實(shí)現(xiàn),方便業(yè)務(wù)方在各種場(chǎng)景使用,這里介紹下python實(shí)現(xiàn)的簡(jiǎn)化版代碼。
在bddispatch.py 中,抽象了調(diào)用方式,提供統(tǒng)一的執(zhí)行接口,統(tǒng)一處理異常和返回結(jié)果。 bddispatch.py
def executeFunc(module_name, func_name, context=None, use_main_interp=True, *args, **kwargs):print( submit call , module_name, . , func_name)if use_main_interp == True:result = Noneexception = Nonetry:m = __import__(module_name)f = getattr(m, func_name)r = f(*args, **kwargs)result = rexcept:exception = traceback.format_exc()singletonExecutorCallback(result, exception, context)else:future = singletonExecutor.submit(_xxsubinterpreters.call_function, module_name, func_name, *args, **kwargs)future.context = contextfuture.add_done_callback(executeDoneCallBack)def executeDoneCallBack(future):r = future.result()e = future.exception()singletonExecutorCallback(r, e, future.context)直接綁定到子解釋器執(zhí)行
對(duì)于性能要求高的場(chǎng)景,通過(guò)上述的方式,由主解釋器調(diào)用子解釋器去執(zhí)行任務(wù)會(huì)增加性能損耗。 這里我們提供了一些CAPI, 讓直接內(nèi)嵌cpython的使用方通過(guò)C API直接綁定某個(gè)解釋器執(zhí)行。
class GILGuard { public:GILGuard() {inter_ = BDPythonVMDispatchGetInterperter();if (inter_ == PyInterpreterState_Main()) {printf( Ensure on main interpreter: %p\n , inter_);} else {printf( Ensure on sub interpreter: %p\n , inter_);}gil_ = PyGILState_EnsureWithInterpreterState(inter_);}~GILGuard() {if (inter_ == PyInterpreterState_Main()) {printf( Release on main interpreter: %p\n , inter_);} else {printf( Release on sub interpreter: %p\n , inter_);}PyGILState_Release(gil_);}private:PyInterpreterState *inter_;PyGILState_STATE gil_; };// 這樣就可以自動(dòng)綁定到一個(gè)解釋器直接執(zhí)行 - (void)testNumpy {GILGuard gil_guard;BDPythonVMRun(....); }關(guān)于字節(jié)跳動(dòng)終端技術(shù)團(tuán)隊(duì)
字節(jié)跳動(dòng)終端技術(shù)團(tuán)隊(duì)(Client Infrastructure)是大前端基礎(chǔ)技術(shù)的全球化研發(fā)團(tuán)隊(duì)(分別在北京、上海、杭州、深圳、廣州、新加坡和美國(guó)山景城設(shè)有研發(fā)團(tuán)隊(duì)),負(fù)責(zé)整個(gè)字節(jié)跳動(dòng)的大前端基礎(chǔ)設(shè)施建設(shè),提升公司全產(chǎn)品線的性能、穩(wěn)定性和工程效率;支持的產(chǎn)品包括但不限于抖音、今日頭條、西瓜視頻、飛書、番茄小說(shuō)等,在移動(dòng)端、Web、Desktop等各終端都有深入研究。
團(tuán)隊(duì)目前招聘 python解釋器優(yōu)化方向的實(shí)習(xí)生,工作內(nèi)容主要為優(yōu)化cpython解釋器,優(yōu)化cpythonJIT(自研),優(yōu)化cpython常用三方庫(kù)。歡迎聯(lián)系 微信:?beyourselfyii。郵箱:?xiejunyi.arch@bytedance.com
🔥 火山引擎 APMPlus 應(yīng)用性能監(jiān)控是火山引擎應(yīng)用開(kāi)發(fā)套件 MARS 下的性能監(jiān)控產(chǎn)品。我們通過(guò)先進(jìn)的數(shù)據(jù)采集與監(jiān)控技術(shù),為企業(yè)提供全鏈路的應(yīng)用性能監(jiān)控服務(wù),助力企業(yè)提升異常問(wèn)題排查與解決的效率。目前我們面向中小企業(yè)特別推出「APMPlus 應(yīng)用性能監(jiān)控企業(yè)助力行動(dòng)」,為中小企業(yè)提供應(yīng)用性能監(jiān)控免費(fèi)資源包。現(xiàn)在申請(qǐng),有機(jī)會(huì)獲得60天免費(fèi)性能監(jiān)控服務(wù),最高可享6000萬(wàn)條事件量。
👉 點(diǎn)擊這里,立即申請(qǐng)
總結(jié)
以上是生活随笔為你收集整理的Python3 cpython优化 实现解释器并行的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java grizzly_Grizzly
- 下一篇: 唐诗三百首出现最多的字是什么?大数据分析