在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
有两件事让我困惑:
%timeit np.arange(n)只需要46.4 µs.这些时间之间有几个数量级.Tho*_*zco 17
我重新编写了一些示例代码来研究这个问题.这是我降落的地方,我将在下面的答案中使用它:
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就是完成工作.
同样,这里重要的是热循环.我们来看看他们.
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_SetItemvs Array_ass_item..
实际上,如果您更改代码而不是Array_init使用(),并重新编译Python,则新结果将变为:Array_ass_itemPySequence_SetItemif (-1 == Array_ass_item((PyObject *)self, i, v))
sct_init
11.504781467840075
sct_subscript
9.381130554247648
ct_init
11.625461496878415
ct_subscript
9.265848568174988
Test
OK
还是有点慢,但不是很多.
换句话说,大部分开销是由较慢的热循环引起的,并且主要是由包裹的代码PySequence_SetItemArray_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_CallArray_ass_item......!换句话说,我们有一个C代码,Array_init它__setitem__在热循环中调用Python代码().那很慢.
现在,为什么Python中使用PySequence_SetItem的Array_init,而不是Array_ass_item在Array_init?
那是因为如果它这样做,它将绕过在Python-land中暴露给开发人员的钩子.
实际上,您可以sh[:] = ...通过继承数组和覆盖来拦截调用__setitem__(__setslice__在Python 2中).它将被调用一次,带有slice索引的参数.
同样,定义自己的__setitem__也会覆盖构造函数中的逻辑.它将被调用N次,索引的整数参数.
这意味着如果Array_init直接调用Array_ass_item,则会丢失一些东西:__setitem__不再在构造函数中调用,并且您将无法再覆盖该行为.
现在我们可以尝试保持更快的速度,同时仍然暴露相同的Python钩子吗?
好吧,也许,使用此代码Array_init而不是现有的热循环:
 return PySequence_SetSlice((PyObject*)self, 0, PyTuple_GET_SIZE(args), args);
使用它将使用slice参数调用__setitem__ 一次(在Python 2上,它将调用__setslice__).我们仍然通过Python钩子,但我们只做了一次而不是N次.
使用此代码,性能变为:
sct_init
12.24651838419959
sct_subscript
10.984305887017399
ct_init
12.138383641839027
ct_subscript
11.79078131634742
Test
OK
我认为其余的开销可能是由于在调用__init__数组对象时发生的元组实例化(请注意*,以及Array_init需要一个元组的事实args) - 这可能n也会缩放.
事实上,如果你更换sh[:] = l与sh[:] = 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.arange在这里实现),我们终于可以明白为什么这么多的速度比sct.RawArray:np.arange不会出现使Python的"用户域"呼叫(即没有调用PySequence_GetItem或PySequence_SetItem).
这并不一定能解释所有的差异,但你可能想开始在那里进行调查.
不是答案(接受的答案很好地解释了这一点),但对于那些寻找如何解决此问题的人,方法如下:不要使用RawArray切片赋值运算符。
正如已接受的答案中所指出的,RawArrays 切片赋值运算符没有利用这样一个事实,即您正在围绕相同类型和大小的 C 样式数组的两个包装器之间进行复制。但是RawArray实现了缓冲区协议,因此您可以将其包装在amemoryview中以“更加原始”的方式访问它(并且它会Foo2获胜,因为您只能在构造对象后执行此操作,而不是作为构造的一部分):
def foo2():
    sh = sct.RawArray(ct.c_int, len(l))
    # l must be another buffer protocol object w/the same C format, which is the case here
    memoryview(sh)[:] = l
    return sh
在另一个问题上解决这个问题的测试中,使用memoryview包装器进行复制的时间不到使用RawArray正常切片分配进行复制所需时间的 1% 。这里的一个技巧是输出元素的大小np.random.randint是np.int,在 64 位系统上np.int是 64 位,所以在 64 位 Python 上,你需要另一轮复制来将它强制为正确的大小(或者你需要将 声明为RawArray与 的大小匹配的类型np.int。即使您确实需要制作该临时副本,使用以下内容仍然便宜得多memoryview:
>>> l = np.random.randint(0, 10, size=100000)
>>> %time sh = sct.RawArray(ct.c_int, len(l))
Wall time: 472 µs  # Creation is cheap
>>> %time sh[:] = l
Wall time: 14.4 ms  # TOO LONG!
# Must convert to numpy array with matching element size when c_int and np.int don't match
>>> %time memoryview(sh)[:] = np.array(l, dtype=np.int32)
Wall time: 424 µs
如您所见,即使您需要先复制np.array以调整元素大小,总时间也少于使用RawArray自己的切片赋值运算符所需时间的 3% 。
如果您通过使RawArray匹配的大小与源匹配来避免临时副本,则成本会进一步下降:
# Make it 64 bit to match size of np.int on my machine
>>> %time sh = sct.RawArray(ct.c_int64, len(l))
Wall time: 522 µs  # Creation still cheap, even at double the size
# No need to convert source array now:
>>> %time memoryview(sh)[:] = l
Wall time: 123 µs
这让我们减少了RawArray切片分配时间的0.85% ;在这一点上,你基本上是在memcpy高速运行;您实际的 Python 代码的其余部分将淹没在数据复制上花费的极少时间。
| 归档时间: | 
 | 
| 查看次数: | 1288 次 | 
| 最近记录: |