找回密码
 会员注册
查看: 11|回复: 0

Python3cpython优化支持解释器并行

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
75213
发表于 2024-9-30 02:58:42 | 显示全部楼层 |阅读模式
技术干货哪里找?点击上方蓝字关注我们!背景在业务场景中,我们通过cpython执行算法包,由于cpython的实现,在一个进程内,无法利用CPU的多个核心去同时执行算法包。对此,我们决定优化cpython,目标是让cpython高完成度的支持并行,大幅度提高单个进程内Python算法包的执行效率。在2020年,我们完成了对cpython的并行执行改造,是目前业界首个cpython3的高完成度同时兼容Python C API的并行实现。性能单线程性能劣化7.7%多线程基本无锁抢占,多开一个线程减少44%的执行时间。并行执行对总执行时间有大幅度的优化通过了cpython的单元测试在线上已经全量使用cpython痛, GILcpython是python官方的解释器实现。在cpython中,GIL,用于保护对Python对象的访问,从而防止多个线程同时执行Python字节码。GIL防止出现竞争情况并确保线程安全。因为GIL的存在,cpython 是无法真正的并行执行python字节码的. GIL虽然限制了python的并行,但是因为cpython的代码没有考虑到并行执行的场景,充满着各种各样的共享变量,改动复杂度太高,官方一直没有移除GIL。挑战在Python开源的20年里,Python 因为GIL(全局锁)不能并行。目前主流实现Python并行的两种技术路线,但是一直没有高完成度的解决方案(高性能,兼容所有开源feature, API稳定)。主要是因为:1. 直接去除GIL 解释器需要加许多细粒度的锁,影响单线程的执行性能,慢两倍。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.2. 解释器状态隔离 解释器内部的实现充满了各种全局状态,改造繁琐,工作量大。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[1],但是目前已经搁置了。目前只能运行非常简单的算术运算的demo。对Type和许多模块的并行执行问题并没有处理,无法在实际场景中使用。新架构-多解释器架构为了实现最佳的执行性能,我们参考multi-core-python[1],在cpython3.10实现了一个高完成度的并行实现。从全局解释器状态 转换为 每个解释器结构持有自己的运行状态(独立的GIL,各种执行状态)。支持并行,解释器状态隔离,并行执行性能不受解释器个数的影响(解释器间基本没有锁相互抢占)通过线程的Thread Specific Data获取Python解释器状态。在这套新架构下,Python的解释器相互隔离,不共享GIL,可以并行执行。充分利用现代CPU的多核性能。大大减少了业务算法代码的执行时间。共享变量的隔离解释器执行中使用了很多共享的变量,他们普遍以全局变量的形式存在.多个解释器运行时,会同时对这些共享变量进行读写操作,线程不安全。cpython内部的主要共享变量:3.10待处理的共享变量[2]。大概有1000个...需要处理,工作量非常之大。free listsMemoryErrorasynchronous generatorcontextdictfloatframelistslicesingletonssmall integer ([-5; 256] range)empty bytes string singletonempty Unicode string singletonempty tuple singletonsingle byte character (b’\x00’ to b’\xFF’)single Unicode character (U+0000-U+00FF range)cacheslide cachemethod cachebigint cache...interned stringsPyUnicode_FromIdstatic strings....如何让每个解释器独有这些变量呢?cpython是c语言实现的,在c中,我们一般会通过 参数中传递 interpreter_state 结构体指针来保存属于一个解释器的成员变量。这种改法也是性能上最好的改法。但是如果这样改,那么所有使用interpreter_state的函数都需要修改函数签名。从工程角度上是几乎无法实现的。只能换种方法,我们可以将interpreter_state存放到thread specific data中。interpreter执行时,通过thread specific key获取到 interpreter_state.这样就可以通过thread specific的API,获取到执行状态,并且不用修改函数的签名。staticinlinePyInterpreterState*_PyInterpreterState_GET(void){PyThreadState*tstate=_PyThreadState_GET();#ifdefPy_DEBUG_Py_EnsureTstateNotNULL(tstate);#endifreturntstate->interp;}共享变量变为解释器单独持有 我们将所有的共享变量存放到 interpreter_state里。/*Smallintegersarepreallocatedinthisarraysothattheycanbeshared.Theintegersthatarepreallocatedarethoseintherange-_PY_NSMALLNEGINTS(inclusive)to_PY_NSMALLPOSINTS(notinclusive).*/PyLongObject*small_ints[_PY_NSMALLNEGINTS+_PY_NSMALLPOSINTS];struct_Py_bytes_statebytes;struct_Py_unicode_stateunicode;struct_Py_float_statefloat_state;/*Usingacacheisveryeffectivesincetypicallyonlyasinglesliceiscreatedandthendeletedagain.*/PySliceObject*slice_cache;struct_Py_tuple_statetuple;struct_Py_list_statelist;struct_Py_dict_statedict_state;struct_Py_frame_stateframe;struct_Py_async_gen_stateasync_gen;struct_Py_context_statecontext;struct_Py_exc_stateexc_state;structast_stateast;structtype_cachetype_cache;#ifndefPY_NO_SHORT_FLOAT_REPRstruct_PyDtoa_Bigint*dtoa_freelist[_PyDtoa_Kmax+1];#endif通过_PyInterpreterState_GET 快速访问。例如/*GetBigintfreelistfrominterpreter*/staticBigint**get_freelist(void){PyInterpreterState*interp=_PyInterpreterState_GET();returninterp->dtoa_freelist;}注意,将全局变量改为thread specific data是有性能影响的,不过只要控制该API调用的次数,性能影响还是可以接受的。我们在cpython3.10已有改动的的基础上,解决了各种各样的共享变量问题,3.10待处理的共享变量[2]。Type变量共享的处理,API兼容性及解决方案目前cpython3.x 暴露了PyType_xxx 类型变量在API中。这些全局类型变量被第三方扩展代码以&yType_xxx的方式引用。如果将Type隔离到子解释器中,势必造成不兼容的问题。这也是官方改动停滞的原因,这个问题无法以合理改动的方式出现在python3中。只能等到python4修改API之后改掉。我们通过另外一种方式快速地改掉了这个问题。Type是共享变量会导致以下的问题1. Type Object的 Ref count被频繁修改,线程不安全2. Type Object 成员变量被修改,线程不安全。改法:1. immortal type object.2. 使用频率低的不安全处加锁。3. 高频使用的场景,使用的成员变量设置为immortal object.针对python的描述符机制,对实际使用时,类型的property,函数,classmethod,staticmethod,doc生成的描述符也设置成immortal object.这样会导致Type和成员变量会内存泄漏。不过由于cpython有module的缓存机制,不清理缓存时,便没有问题。pymalloc内存池共享处理我们使用了mimalloc[3]替代pymalloc内存池,在优化1%-2%性能的同时,也不需要额外处理pymalloc。subinterperter 能力补全官方master最新代码 subinterpreter 模块只提供了interp_run_string可以执行code_string. 出于体积和安全方面的考虑,我们已经删除了python动态执行code_string的功能。我们给subinterpreter模块添加了两个额外的能力1. interp_call_file 调用执行python pyc文件2. interp_call_function 执行任意函数subinterpreter 执行模型python中,我们执行代码默认运行的是main interpreter, 我们也可以创建的sub interpreter执行代码,interp=_xxsubinterpreters.create()result=_xxsubinterpreters.interp_call_function(*args,**kwargs)这里值得注意的是,我们是在 main interpreter 创建 sub interpreter,随后在sub interpreter 执行,最后把结果返回到main interpreter. 这里看似简单,但是做了很多事情。1. main interpreter 将参数传递到 sub interpreter2. 线程切换到 sub interpreter的 interpreter_state。获取并转换参数3. sub interpreter 解释执行代码4. 获取返回值,切换到main interpreter5. 转换返回值6. 异常处理这里有两个复杂的地方:1. interpreter state 状态的切换2. interpreter 数据的传递interpreter state 状态的切换interp=_xxsubinterpreters.create()result=_xxsubinterpreters.interp_call_function(*args,**kwargs)我们可以分解为#RunningInthread11:#maininterpreter:#现在threadspecific设置的interpreterstate是maininterpreter的dosomethings...createsubinterpreter...interp_call_function...#threadspecific设置interpreterstate为subinterpreterstate#subinterpreter:dosomethins...callfunction...getresult...#现在threadspecific设置interpreterstate为maininterpreterstategetreturnresult..interpreter 数据的传递因为我们解释器的执行状态是隔离的,在main interpreter 中创建的 Python Object是无法在 sub interpreter 使用的. 我们需要:1. 获取 main interpreter 的 PyObject 关键数据2. 存放在 一块内存中3. 在sub interpreter 中根据该数据重新创建 PyObjectinterpreter 状态的切换 & 数据的传递 的实现可以参考以下示例 ...staticPyObject*_call_function_in_interpreter(PyObject*self,PyInterpreterState*interp,_sharedns*args_shared,_sharedns*kwargs_shared){PyObject*result=NULLyObject*exctype=NULLyObject*excval=NULLyObject*tb=NULL;_sharedns*result_shread=_sharedns_new(1);#ifdefEXPERIMENTAL_ISOLATED_SUBINTERPRETERS//Switchtointerpreter.PyThreadState*new_tstate=PyInterpreterState_ThreadHead(interp)yThreadState*save1=PyEval_SaveThread();(void)PyThreadState_Swap(new_tstate);#else//Switchtointerpreter.PyThreadState*save_tstate=NULL;if(interp!=PyInterpreterState_Get()){//XXXUsingtheheadthreadisn'tstrictlycorrect.PyThreadState*tstate=PyInterpreterState_ThreadHead(interp);//XXXPossibleGILStateissuessave_tstate=PyThreadState_Swap(tstate);}#endifPyObject*module_name=_PyCrossInterpreterData_NewObject(&args_shared->items[0].data)yObject*function_name=_PyCrossInterpreterData_NewObject(&args_shared->items[1].data);...PyObject*module=PyImport_ImportModule(PyUnicode_AsUTF8(module_name))yObject*function=PyObject_GetAttr(module,function_name);result=PyObject_Call(function,args,kwargs);...#ifdefEXPERIMENTAL_ISOLATED_SUBINTERPRETERS//Switchback.PyEval_RestoreThread(save1);#else//Switchback.if(save_tstate!=NULL){PyThreadState_Swap(save_tstate);}#endifif(result){result=_PyCrossInterpreterData_NewObject(&result_shread->items[0].data);_sharedns_free(result_shread);}returnresult;}实现子解释器池我们已经实现了内部的隔离执行环境,但是这是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=contextfuture.add_done_callback(executeDoneCallBack)我们内部是这样实现的: 继承 concurrent 提供的 Executor 基类classSubInterpreterPoolExecutor(_base.Executor):SubInterpreterPool 初始化时创建线程,并且每个线程创建一个 sub interpreterinterp=_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的改动较大,存在两个隐患1. 代码可能存在兼容性问题,第三方C/C++ Extension 实现存在全局状态变量,非线程安全。2. python存在着极少的一些模块.sub interpreter无法使用。例如process我们希望能统一对外的接口,让使用者不需要关注这些细节,我们自动的切换调用方式。自动选择在主解释器使用(兼容性好,稳定)还是子解释器(支持并行,性能佳)我们提供了C和python的实现,方便业务方在各种场景使用,这里介绍下python实现的简化版代码。在bddispatch.py 中,抽象了调用方式,提供统一的执行接口,统一处理异常和返回结果。bddispatch.pydefexecuteFunc(module_name,func_name,context=None,use_main_interp=True,*args,**kwargs):print(submitcall,module_name,.,func_name)ifuse_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)defexecuteDoneCallBack(future):r=future.result()e=future.exception()singletonExecutorCallback(r,e,future.context)直接绑定到子解释器执行对于性能要求高的场景,通过上述的方式,由主解释器调用子解释器去执行任务会增加性能损耗。这里我们提供了一些CAPI, 让直接内嵌cpython的使用方通过C API直接绑定某个解释器执行。classGILGuard{public:GILGuard(){inter_=BDPythonVMDispatchGetInterperter();if(inter_==PyInterpreterState_Main()){printf(Ensureonmaininterpreter:%p\n,inter_);}else{printf(Ensureonsubinterpreter:%p\n,inter_);}gil_=PyGILState_EnsureWithInterpreterState(inter_);}~GILGuard(){if(inter_==PyInterpreterState_Main()){printf(Releaseonmaininterpreter:%p\n,inter_);}else{printf(Releaseonsubinterpreter:%p\n,inter_);}PyGILState_Release(gil_);}privateyInterpreterState*inter_yGILState_STATEgil_;};//这样就可以自动绑定到一个解释器直接执行-(void)testNumpy{GILGuardgil_guard;BDPythonVMRun(....);}参考文献1.multi-core-python[1]:https://github.com/ericsnowcurrently/multi-core-python2.3.10待处理的共享变量[2]:https://github.com/python/cpython/blob/3.10/Tools/c-analyzer/TODO3. mimalloc[3]:https://github.com/microsoft/mimalloc# 关于字节终端技术团队字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、番茄小说等,在移动端、Web、Desktop等各终端都有深入研究。团队目前招聘 python解释器优化方向的实习生,工作内容主要为优化cpython解释器,优化cpythonJIT(自研),优化cpython常用三方库。欢迎联系 微信: beyourselfyii。邮箱:xiejunyi.arch@bytedance.comMARS- TALK 04 期来啦!2月24日晚 MARS TALK 直播间,我们邀请了火山引擎 APMPlus 和美篇的研发工程师,在线为大家分享「APMPlus 基于 Hprof 文件的 Java OOM 归因方案」及「美篇基于MARS-APMPlus 性能监控工具的优化实践」等技术干货。现在报名加入活动群 还有机会获得最新版VR一体机——Pico Neo3哦! 直播时间:2月24日(周四) 20:00-21:30 活动形式:线上直播 报名方式:扫码进群报名作为开年首期MARS TALK,本次我们为大家准备了丰厚的奖品。除了Pico Neo3之外,还有罗技M720蓝牙鼠标、筋膜枪及字节周边礼品等你来拿。千万不要错过哟!报名赢大奖惊喜好礼带回家点击阅读原文,了解APMPlus
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-16 00:16 , Processed in 1.213106 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表