Python3 cpython优化 实现解释器并行
本文介紹了對cpython解釋器的并行優化,使其支持真正的多解釋器并行執行的解決方案。
作者:字節跳動終端技術——謝俊逸
背景
在業務場景中,我們通過cpython執行算法包,由于cpython的實現,在一個進程內,無法利用CPU的多個核心去同時執行算法包。對此,我們決定優化cpython,目標是讓cpython高完成度的支持并行,大幅度的提高單個進程內Python算法包的執行效率。
在2020年,我們完成了對cpython的并行執行改造,是目前業界首個cpython3的高完成度同時兼容Python C API的并行實現。
-
性能
- 單線程性能劣化7.7%
- 多線程基本無鎖搶占,多開一個線程減少44%的執行時間。
- 并行執行對總執行時間有大幅度的優化
-
通過了cpython的單元測試
-
在線上已經全量使用
cpython痛, GIL
cpython是python官方的解釋器實現。在cpython中,GIL,用于保護對Python對象的訪問,從而防止多個線程同時執行Python字節碼。GIL防止出現競爭情況并確保線程安全。 因為GIL的存在,cpython 是無法真正的并行執行python字節碼的. GIL雖然限制了python的并行,但是因為cpython的代碼沒有考慮到并行執行的場景,充滿著各種各樣的共享變量,改動復雜度太高,官方一直沒有移除GIL。
挑戰
在Python開源的20年里,Python 因為GIL(全局鎖)不能并行。目前主流實現Python并行的兩種技術路線,但是一直沒有高完成度的解決方案(高性能,兼容所有開源feature, API穩定)。主要是因為:
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.
這個思路開源有一個項目在做 multi-core-python,但是目前已經擱置了。目前只能運行非常簡單的算術運算的demo。對Type和許多模塊的并行執行問題并沒有處理,無法在實際場景中使用。
新架構-多解釋器架構
為了實現最佳的執行性能,我們參考multi-core-python,在cpython3.10實現了一個高完成度的并行實現。
- 從全局解釋器狀態 轉換為 每個解釋器結構持有自己的運行狀態(獨立的GIL,各種執行狀態)。
- 支持并行,解釋器狀態隔離,并行執行性能不受解釋器個數的影響(解釋器間基本沒有鎖相互搶占)
- 通過線程的Thread Specific Data獲取Python解釋器狀態。
在這套新架構下,Python的解釋器相互隔離,不共享GIL,可以并行執行。充分利用現代CPU的多核性能。大大減少了業務算法代碼的執行時間。
共享變量的隔離
解釋器執行中使用了很多共享的變量,他們普遍以全局變量的形式存在.多個解釋器運行時,會同時對這些共享變量進行讀寫操作,線程不安全。
cpython內部的主要共享變量:3.10待處理的共享變量。大概有1000個…需要處理,工作量非常之大。
-
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
-
…
如何讓每個解釋器獨有這些變量呢?
cpython是c語言實現的,在c中,我們一般會通過 參數中傳遞 interpreter_state 結構體指針來保存屬于一個解釋器的成員變量。這種改法也是性能上最好的改法。但是如果這樣改,那么所有使用interpreter_state的函數都需要修改函數簽名。從工程角度上是幾乎無法實現的。
只能換種方法,我們可以將interpreter_state存放到thread specific data中。interpreter執行時,通過thread specific key獲取到 interpreter_state.這樣就可以通過thread specific的API,獲取到執行狀態,并且不用修改函數的簽名。
static inline PyInterpreterState* _PyInterpreterState_GET(void) {PyThreadState *tstate = _PyThreadState_GET(); #ifdef Py_DEBUG_Py_EnsureTstateNotNULL(tstate); #endifreturn tstate->interp; }共享變量變為解釋器單獨持有 我們將所有的共享變量存放到 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通過 _PyInterpreterState_GET 快速訪問。 例如
/* Get Bigint freelist from interpreter */ static Bigint ** get_freelist(void) {PyInterpreterState *interp = _PyInterpreterState_GET();return interp->dtoa_freelist; }注意,將全局變量改為thread specific data是有性能影響的,不過只要控制該API調用的次數,性能影響還是可以接受的。 我們在cpython3.10已有改動的的基礎上,解決了各種各樣的共享變量問題,3.10待處理的共享變量
Type變量共享的處理,API兼容性及解決方案
目前cpython3.x 暴露了PyType_xxx 類型變量在API中。這些全局類型變量被第三方擴展代碼以&PyType_xxx的方式引用。如果將Type隔離到子解釋器中,勢必造成不兼容的問題。這也是官方改動停滯的原因,這個問題無法以合理改動的方式出現在python3中。只能等到python4修改API之后改掉。
我們通過另外一種方式快速的改掉了這個問題。
Type是共享變量會導致以下的問題
改法:
immortal type object.
使用頻率低的不安全處加鎖。
高頻使用的場景,使用的成員變量設置為immortal object.
這樣會導致Type和成員變量會內存泄漏。不過由于cpython有module的緩存機制,不清理緩存時,便沒有問題。
pymalloc內存池共享處理
我們使用了mimalloc替代pymalloc內存池,在優化1%-2%性能的同時,也不需要額外處理pymalloc。
subinterperter 能力補全
官方master最新代碼 subinterpreter 模塊只提供了interp_run_string可以執行code_string. 出于體積和安全方面的考慮,我們已經刪除了python動態執行code_string的功能。 我們給subinterpreter模塊添加了兩個額外的能力
subinterpreter 執行模型
python中,我們執行代碼默認運行的是main interpreter, 我們也可以創建的sub interpreter執行代碼,
interp = _xxsubinterpreters.create() result = _xxsubinterpreters.interp_call_function(*args, **kwargs)這里值得注意的是,我們是在 main interpreter 創建 sub interpreter, 隨后在sub interpreter 執行,最后把結果返回到main interpreter. 這里看似簡單,但是做了很多事情。
這里有兩個復雜的地方:
interpreter state 狀態的切換
interp = _xxsubinterpreters.create() result = _xxsubinterpreters.interp_call_function(*args, **kwargs)我們可以分解為
# Running In thread 11: # main interpreter: # 現在 thread specific 設置的 interpreter state 是 main interpreter的 do some things ... create subinterpreter ... interp_call_function ... # thread specific 設置 interpreter state 為 sub interpreter state # sub interpreter: do some thins ... call function ... get result ... # 現在 thread specific 設置 interpreter state 為 main interpreter state get return result ...interpreter 數據的傳遞
因為我們解釋器的執行狀態是隔離的,在main interpreter 中創建的 Python Object是無法在 sub interpreter 使用的. 我們需要:
interpreter 狀態的切換 & 數據的傳遞 的實現可以參考以下示例 …
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; }實現子解釋器池
我們已經實現了內部的隔離執行環境,但是這是API比較低級,需要封裝一些高度抽象的API,提高子解釋器并行的易用能力。
interp = _xxsubinterpreters.create() result = _xxsubinterpreters.interp_call_function(*args, **kwargs)這里我們參考了,python concurrent庫提供的 thread pool, process pool, futures的實現,自己實現了 subinterpreter pool. 通過concurrent.futures 模塊提供異步執行回調高層接口。
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)我們內部是這樣實現的: 繼承 concurrent 提供的 Executor 基類
class SubInterpreterPoolExecutor(_base.Executor):SubInterpreterPool 初始化時創建線程,并且每個線程創建一個 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 接收參數,并使用 interp 執行
result = self.fn(self.interp ,*self.args, **self.kwargs)實現外部調度模塊
針對sub interpreter的改動較大,存在兩個隱患
我們希望能統一對外的接口,讓使用者不需要關注這些細節,我們自動的切換調用方式。自動選擇在主解釋器使用(兼容性好,穩定)還是子解釋器(支持并行,性能佳)
我們提供了C和python的實現,方便業務方在各種場景使用,這里介紹下python實現的簡化版代碼。
在bddispatch.py 中,抽象了調用方式,提供統一的執行接口,統一處理異常和返回結果。 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)直接綁定到子解釋器執行
對于性能要求高的場景,通過上述的方式,由主解釋器調用子解釋器去執行任務會增加性能損耗。 這里我們提供了一些CAPI, 讓直接內嵌cpython的使用方通過C API直接綁定某個解釋器執行。
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_; };// 這樣就可以自動綁定到一個解釋器直接執行 - (void)testNumpy {GILGuard gil_guard;BDPythonVMRun(....); }關于字節跳動終端技術團隊
字節跳動終端技術團隊(Client Infrastructure)是大前端基礎技術的全球化研發團隊(分別在北京、上海、杭州、深圳、廣州、新加坡和美國山景城設有研發團隊),負責整個字節跳動的大前端基礎設施建設,提升公司全產品線的性能、穩定性和工程效率;支持的產品包括但不限于抖音、今日頭條、西瓜視頻、飛書、番茄小說等,在移動端、Web、Desktop等各終端都有深入研究。
團隊目前招聘 python解釋器優化方向的實習生,工作內容主要為優化cpython解釋器,優化cpythonJIT(自研),優化cpython常用三方庫。歡迎聯系 微信:?beyourselfyii。郵箱:?xiejunyi.arch@bytedance.com
🔥 火山引擎 APMPlus 應用性能監控是火山引擎應用開發套件 MARS 下的性能監控產品。我們通過先進的數據采集與監控技術,為企業提供全鏈路的應用性能監控服務,助力企業提升異常問題排查與解決的效率。目前我們面向中小企業特別推出「APMPlus 應用性能監控企業助力行動」,為中小企業提供應用性能監控免費資源包。現在申請,有機會獲得60天免費性能監控服務,最高可享6000萬條事件量。
👉 點擊這里,立即申請
總結
以上是生活随笔為你收集整理的Python3 cpython优化 实现解释器并行的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java grizzly_Grizzly
- 下一篇: cpython和ipython_关于cp