关于字节跳动: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稳固)。次要是因为:

  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.

  1. 解释器状态隔离 解释器外部的实现充斥了各种全局状态,革新繁琐,工作量大。

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);
#endif
    return tstate->interp;
}

共享变量变为解释器独自持有 咱们将所有的共享变量寄存到 interpreter_state里。

    /* Small integers are preallocated in this array so that they
       can 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 is
       created 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_REPR
    struct _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是共享变量会导致以下的问题

  1. Type Object的 Ref count被频繁批改,线程不平安
  2. Type Object 成员变量被批改,线程不平安。

改法:

  1. immortal type object.
  2. 应用频率低的不平安处加锁。
  3. 高频应用的场景,应用的成员变量设置为immortal object.

    1. 针对python的描述符机制,对理论应用时,类型的property,函数,classmethod,staticmethod,doc生成的描述符也设置成immortal object.

这样会导致Type和成员变量会内存透露。不过因为cpython有module的缓存机制,不清理缓存时,便没有问题。

pymalloc内存池共享解决

咱们应用了mimalloc代替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 interpreter
  2. 线程切换到 sub interpreter的 interpreter_state。获取并转换参数
  3. sub interpreter 解释执行代码
  4. 获取返回值,切换到main interpreter
  5. 转换返回值
  6. 异样解决

这里有两个简单的中央:

  1. interpreter state 状态的切换
  2. 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 应用的. 咱们须要:

  1. 获取 main interpreter 的 PyObject 要害数据
  2. 寄存在 一块内存中
  3. 在sub interpreter 中依据该数据从新创立 PyObject

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);
    }
#endif
    
    PyObject *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);
    }
#endif
    
    if (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的改变较大,存在两个隐患

  1. 代码可能存在兼容性问题,第三方C/C++ Extension 实现存在全局状态变量,非线程平安。
  2. python存在着极少的一些模块.sub interpreter无奈应用。例如process

咱们心愿能对立对外的接口,让使用者不须要关注这些细节,咱们主动的切换调用形式。主动抉择在主解释器应用(兼容性好,稳固)还是子解释器(反对并行,性能佳)

咱们提供了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 = None
        exception = None
        try:
            m = __import__(module_name)
            f = getattr(m, func_name)
            r = f(*args, **kwargs)
            result = r
        except:
            exception = traceback.format_exc()
        singletonExecutorCallback(result, exception, context)

    else:
        future = singletonExecutor.submit(_xxsubinterpreters.call_function, module_name, func_name, *args, **kwargs)
        future.context = context
        future.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万条事件量。

👉 点击这里,立刻申请

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理