在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
。这些时间之间有几个数量级。答案 0 :(得分: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
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
后才能完全绕过!)< / p>
通过兔子洞,呼叫序列看起来有点像这样:
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_SetItem
中使用Array_init
而在Array_ass_item
中不使用Array_init
?
那是因为如果确实如此,它将绕过在Python-land中暴露给开发人员的钩子。
实际上,可以通过继承数组并覆盖sh[:] = ...
(Python 2中的__setitem__
)来拦截对__setslice__
的调用。它将被调用一次,索引的参数为slice
。
同样,定义自己的__setitem__
也会覆盖构造函数中的逻辑。它将被调用N次,索引的整数参数。
这意味着,如果Array_init
直接调入Array_ass_item
,那么您将丢失一些内容:__setitem__
将不再在构造函数中调用,并且您将无法覆盖这种行为了。
现在我们可以尝试保持更快的速度,同时仍然暴露相同的Python钩子吗?
好吧,也许,在Array_init
中使用此代码而不是现有的热循环:
return PySequence_SetSlice((PyObject*)self, 0, PyTuple_GET_SIZE(args), args);
使用此方法将使用切片参数调用__setitem__
一次(在Python 2上,它将调用__setslice__
)。我们仍然通过Python钩子,但我们只做了一次而不是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
期望{{1}的元组的事实}}) - 这可能也会与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
进行热循环并查看结果可能会很有趣!
现在,关于第二个问题,关于分配共享内存。
请注意,分配共享内存并不需要花费任何成本。如上面的结果所示,使用共享内存之间没有实质性差异。
查看Numpy代码(Array_init
是implemented here),我们终于可以理解为什么它比np.arange
快得多: sct.RawArray
没有出现调用Python“user-land”(即不调用np.arange
或PySequence_GetItem
)。
这并不一定能解释所有的差异,但你可能想开始在那里进行调查。
答案 1 :(得分:3)
不是答案(the accepted answer很好地解释了这一点),但对于那些正在寻找解决方法的人,请查看以下方法:不要使用RawArray
s切片赋值运算符。
正如the accepted answer中所述,RawArray
的切片赋值运算符并没有利用您在相同类型的C样式数组的两个包装器之间进行复制的事实。尺寸。但是RawArray
实现了缓冲协议,因此您可以将其包装在a memoryview
中以便在&#34;更加原始的&#34;中访问它。方式(并且它会使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
在测试solving this problem on another question中,使用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代码将浪费在数据复制上花费的微不足道的时间。
答案 2 :(得分:0)
这应该是一条评论,但我的声誉不高:-(
从Python 3.5开始,Linux中的共享阵列被创建为映射到内存的临时文件(请参见https://bugs.python.org/issue30919)。我认为这可以解释为什么创建在内存中创建的Numpy数组比创建和初始化大型共享数组要快。 为了强制Python使用共享内存,一种解决方法是执行以下两行代码(参考No space left while using Multiprocessing.Array in shared memory):
from multiprocessing.process import current_process
current_process()._config[‘tempdir’] = ‘/dev/shm’