gpt4 book ai didi

python - 揭秘 sharedctypes 的性能

转载 作者:太空狗 更新时间:2023-10-29 17:27:43 28 4
gpt4 key购买 nike

在 python 中,可以在多个进程之间共享 ctypes 对象。但是我注意到分配这些对象似乎非常昂贵。

考虑以下代码:

from multiprocessing import sharedctypes as sct
import ctypes as ct
import numpy as np

n = 100000
l = np.random.randint(0, 10, size=n)

def foo1():
sh = sct.RawArray(ct.c_int, l)
return sh

def foo2():
sh = sct.RawArray(ct.c_int, len(l))
sh[:] = l
return sh

%timeit foo1()
%timeit foo2()

sh1 = foo1()
sh2 = foo2()

for i in range(n):
assert sh1[i] == sh2[i]

输出是:
10 loops, best of 3: 30.4 ms per loop
100 loops, best of 3: 9.65 ms per loop

有两件事让我感到困惑:
  • 与传递 numpy 数组相比,为什么显式分配和初始化要快得多?
  • 为什么在python中分配共享内存如此昂贵? %timeit np.arange(n)只需要46.4 µs .这些时间之间有几个数量级。
  • 最佳答案

    示例代码

    我稍微重写了你的示例代码来研究这个问题。这是我降落的地方,我将在下面的答案中使用它:
    so.py :

    from multiprocessing import sharedctypes as sct
    import ctypes as ct
    import numpy as np

    n = 100000
    l = np.random.randint(0, 10, size=n)


    def sct_init():
    sh = sct.RawArray(ct.c_int, l)
    return sh

    def sct_subscript():
    sh = sct.RawArray(ct.c_int, n)
    sh[:] = l
    return sh

    def ct_init():
    sh = (ct.c_int * n)(*l)
    return sh

    def ct_subscript():
    sh = (ct.c_int * n)(n)
    sh[:] = l
    return sh

    请注意,我添加了两个不使用共享内存的测试用例(而是使用常规的 ctypes 数组)。
    timer.py :
    import traceback
    from timeit import timeit

    for t in ["sct_init", "sct_subscript", "ct_init", "ct_subscript"]:
    print(t)
    try:
    print(timeit("{0}()".format(t), setup="from so import {0}".format(t), number=100))
    except Exception as e:
    print("Failed:", e)
    traceback.print_exc()
    print

    print()

    print ("Test",)
    from so import *
    sh1 = sct_init()
    sh2 = sct_subscript()

    for i in range(n):
    assert sh1[i] == sh2[i]
    print("OK")

    检测结果

    使用 Python 3.6a0(特别是 3c2fbdb )运行上述代码的结果是:
    sct_init
    2.844902500975877
    sct_subscript
    0.9383537038229406
    ct_init
    2.7903486443683505
    ct_subscript
    0.978101353161037

    Test
    OK

    有趣的是,如果你改变 n ,结果呈线性比例。例如,使用 n = 100000 (大 10 倍),你得到的东西几乎慢了 10 倍:
    sct_init
    30.57974253082648
    sct_subscript
    9.48625904135406
    ct_init
    30.509132395964116
    ct_subscript
    9.465419146697968

    Test
    OK

    速度差

    最后,速度差异在于通过将 Numpy 数组 ( l ) 中的每个值复制到新数组 ( sh ) 来调用以初始化数组的热循环。这是有道理的,因为正如我们所指出的,速度与数组大小呈线性关系。

    当您将 Numpy 数组作为构造函数参数传递时,执行此操作的函数是 Array_init .但是,如果您使用 sh[:] = l 进行分配,那么它是 Array_ass_subscript that does the job .

    同样,这里重要的是热循环。让我们来看看它们。
    Array_init热循环(较慢):
    for (i = 0; i < n; ++i) {
    PyObject *v;
    v = PyTuple_GET_ITEM(args, i);
    if (-1 == PySequence_SetItem((PyObject *)self, i, v))
    return -1;
    }
    Array_ass_subscript热循环(更快):
    for (cur = start, i = 0; i < otherlen; cur += step, i++) {
    PyObject *item = PySequence_GetItem(value, i);
    int result;
    if (item == NULL)
    return -1;
    result = Array_ass_item(myself, cur, item);
    Py_DECREF(item);
    if (result == -1)
    return -1;
    }

    事实证明,大部分速度差异在于使用 PySequence_SetItem对比 Array_ass_item .

    确实,如果您更改 Array_init 的代码使用 Array_ass_item而不是 PySequence_SetItem ( if (-1 == Array_ass_item((PyObject *)self, i, v)) ),重新编译Python,新结果变成:
    sct_init
    11.504781467840075
    sct_subscript
    9.381130554247648
    ct_init
    11.625461496878415
    ct_subscript
    9.265848568174988

    Test
    OK

    仍然有点慢,但不是很多。

    换句话说,大部分开销是由较慢的热循环引起的,主要是由 the code that PySequence_SetItem wraps around Array_ass_item 引起的。 .

    这段代码在第一次阅读时可能看起来开销很小,但实际上并非如此。
    PySequence_SetItem实际上调用整个 Python 机器来解决 __setitem__方法并调用它。

    这最终通过拨打 Array_ass_item 得到解决。 ,但只有在大量间接级别之后(直接调用 Array_ass_item 将完全绕过!)

    穿过兔子洞,调用序列看起来有点像这样:
  • s->ob_type->tp_as_sequence->sq_ass_item指向 slot_sq_ass_item .
  • slot_sq_ass_item调用 call_method .
  • call_method调用 PyObject_Call
  • 如此下去,直到我们最终到达 Array_ass_item ..!

  • 换句话说,我们在 Array_init 中有 C 代码。这是在热循环中调用 Python 代码( __setitem__ )。那很慢。

    为什么 ?

    现在,为什么 Python 使用 PySequence_SetItemArray_init而不是 Array_ass_itemArray_init ?

    这是因为如果这样做,它将绕过在 Python-land 中暴露给开发人员的钩子(Hook)。

    实际上,您可以拦截对 sh[:] = ... 的调用。通过子类化数组并覆盖 __setitem__ ( __setslice__ 在 Python 2 中)。它将被调用一次,带有 slice索引的参数。

    同样,定义您自己的 __setitem__还覆盖构造函数中的逻辑。它将被调用 N 次,索引的整数参数。

    这意味着如果 Array_init直接调用成 Array_ass_item ,那么你会失去一些东西: __setitem__将不再在构造函数中被调用,并且您将无法再覆盖该行为。

    现在我们可以尝试保持更快的速度,同时仍然暴露相同的 Python 钩子(Hook)吗?

    好吧,也许,在 Array_init 中使用此代码而不是现有的热循环:
     return PySequence_SetSlice((PyObject*)self, 0, PyTuple_GET_SIZE(args), args);

    使用它会调用 __setitem__ 一次 带有切片参数(在 Python 2 上,它会调用 __setslice__ )。我们仍然通过 Python 钩子(Hook),但我们只做一次而不是 N 次。

    使用此代码,性能变为:
    sct_init
    12.24651838419959
    sct_subscript
    10.984305887017399
    ct_init
    12.138383641839027
    ct_subscript
    11.79078131634742

    Test
    OK

    其他开销

    我认为其余的开销可能是由于发生的元组实例化 when calling __init__ on the array object (注意 * ,以及 Array_init 期望 args 的元组这一事实)——这大概与 n 成比例以及。

    确实,如果你更换 sh[:] = lsh[:] = tuple(l)在测试用例中,性能结果几乎相同。与 n = 100000 :
    sct_init
    11.538272527977824
    sct_subscript
    10.985187001060694
    ct_init
    11.485244687646627
    ct_subscript
    10.843198659364134

    Test
    OK

    可能还有一些较小的事情发生,但最终我们正在比较两个截然不同的热循环。几乎没有理由期望它们具有相同的性能。

    我认为尝试拨打 Array_ass_subscript 可能会很有趣来自 Array_init但是,对于热循环并查看结果!

    基线速度

    现在,关于您的第二个问题,关于分配共享内存。

    请注意,分配共享内存并没有真正的成本。正如上面的结果所指出的,使用与不使用共享内存之间没有实质性区别。

    查看 Numpy 代码( np.arangeimplemented here ),我们终于明白为什么它比 sct.RawArray 快这么多了: np.arange似乎没有调用 Python“用户空间” (即不拨打 PySequence_GetItemPySequence_SetItem )。

    这并不一定能解释所有差异,但您可能想从那里开始调查。

    关于python - 揭秘 sharedctypes 的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33853543/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com