【问题标题】:Why are multiprocessing.sharedctypes assignments so slow?为什么 multiprocessing.sharedctypes 分配这么慢?
【发布时间】:2016-10-08 22:14:35
【问题描述】:

这里有一个小基准代码来说明我的问题:

import numpy as np
import multiprocessing as mp
# allocate memory
%time temp = mp.RawArray(np.ctypeslib.ctypes.c_uint16, int(1e8))
Wall time: 46.8 ms
# assign memory, very slow
%time temp[:] = np.arange(1e8, dtype = np.uint16)
Wall time: 10.3 s
# equivalent numpy assignment, 100X faster
%time a = np.arange(1e8, dtype = np.uint16)
Wall time: 111 ms

基本上,我希望在多个进程之间共享一个 numpy 数组,因为它很大并且是只读的。 This method 效果很好,没有制作额外的副本,并且进程的实际计算时间很好。但是创建共享数组的开销是巨大的。

This post 提供了一些关于为什么某些初始化数组的方法很慢的深刻见解(请注意,在上面的示例中,我使用的是更快的方法)。但这篇文章并没有真正描述如何真正提高速度到类似 numpy 的性能。

有人对如何提高速度有任何建议吗?一些 cython 代码对分配数组有意义吗?

我正在使用 Windows 7 x64 系统。

【问题讨论】:

    标签: python multiprocessing shared-memory


    【解决方案1】:

    只需在共享数组周围放置一个 numpy 数组即可:

    import numpy as np
    import multiprocessing as mp
    
    sh = mp.RawArray('i', int(1e8))
    x = np.arange(1e8, dtype=np.int32)
    sh_np = np.ctypeslib.as_array(sh)
    

    那么时间:

    %time sh[:] = x
    CPU times: user 10.1 s, sys: 132 ms, total: 10.3 s
    Wall time: 10.2 s
    
    %time memoryview(sh).cast('B').cast('i')[:] = x
    CPU times: user 64 ms, sys: 132 ms, total: 196 ms
    Wall time: 196 ms
    
    %time sh_np[:] = x
    CPU times: user 92 ms, sys: 104 ms, total: 196 ms
    Wall time: 196 ms
    

    无需弄清楚如何投射内存视图(就像我在 python3 Ubuntu 16 中必须做的那样)和搞乱重塑(如果 x 有更多维度,因为 cast() 变平了)。并使用 sh_np.dtype.name 像任何 numpy 数组一样仔细检查数据类型。 :)

    【讨论】:

    • 除非我遗漏了一些明显的东西,比如这在 Windows 中不起作用?
    【解决方案2】:

    在 ms-windows 上,当您创建 Process 时,将生成一个新的 Python 解释器,然后 将您的程序作为模块导入。 (这就是为什么在 ms-windows 上你应该只在 if __name__ is "__main__" 块中创建 ProcessPool。)这将重新创建你的数组,这应该与最初创建它的时间差不多。请参阅programming guidelines,尤其是关于必须在 ms-windows 上使用的 spawn 启动方法。

    所以可能更好的方法是使用numpy.memmap 创建一个内存映射的numpy 数组。在父进程中将数组写入磁盘。 (在 ms-windows 上,这个必须if __name__ is "__main__" 块中完成,所以它只被称为 once)。然后在target函数中以只读模式使用numpy.memmap读取数据。

    【讨论】:

    • 将其作为参数传递给Process 构造函数将pickle 并将其发送给子级以反序列化副本,它不会获得“免费”写时复制@987654335 @语义(它不会被“继承”),即使您使用的是fork 支持操作系统。如果您在支持操作系统的fork 上并且不支持其他操作系统也可以,那么您需要在生成Process 之前的某个时间在全局变量中初始化对象,并且孩子们可以正常访问它。您不希望在 main 函数中定义它,因为这样它在 main 之外不可见,即使在子进程中也是如此。
    • 我做了一些额外的阅读,在 ms-windows 上我认为使用numpy.memmap 是一个更好的解决方案。因此改变了答案。
    • memmaps 可以工作,这是joblib 用来在进程之间共享数据的实现。但是,如果您定期访问共享数据,它的速度会非常缓慢,而且速度提升最多也很少。
    • 我从未使用过 joblib,我选择的平台是 UNIX 而不是 ms-windows。在 UNIX 上,内存映射文件非常快。当然,前提是你有足够的 RAM。
    【解决方案3】:

    由于your second link 中给出的原因,这很慢,而解决方案实际上非常简单:绕过(慢)RawArray 切片分配代码,在这种情况下,读取效率很低每次从源数组中提取原始 C 值以创建 Python 对象,然后将其直接转换回原始 C 以存储在共享数组中,然后丢弃临时 Python 对象,并重复 1e8 次。

    但你不需要那样做;像大多数 C 级别的东西一样,RawArray 实现了缓冲区协议,这意味着您可以将其转换为 memoryview, a view of the underlying raw memory that implements most operations in C-like ways,如果可能的话,使用原始内存操作。所以不要这样做:

    # assign memory, very slow
    %time temp[:] = np.arange(1e8, dtype = np.uint16)
    Wall time: 9.75 s  # Updated to what my machine took, for valid comparison
    

    使用memoryview 将其作为原始字节类对象进行操作并以这种方式分配(np.arange 已经实现了缓冲区协议,memoryview 的切片分配运算符无缝使用它):

    # C-like memcpy effectively, very fast
    %time memoryview(temp)[:] = np.arange(1e8, dtype = np.uint16)
    Wall time: 74.4 ms  # Takes 0.76% of original time!!!
    

    注意,后者的时间是毫秒,不是秒;使用 memoryview 包装进行复制以执行原始内存传输只需不到 1% 的时间即可完成,而 RawArray 默认情况下会这样做!

    【讨论】:

    • 请注意,执行“原始”数据复制时,元素大小必须匹配;如果他们不这样做,您将收到来自memoryviewTypeError 抱怨“项目尺寸不匹配”;如果无法创建具有匹配大小的对象,则在不同的 numpy 数组类型之间进行转换通常相当便宜,因此在 64 位系统上,如果 tempctypes.uint 组成(在大多数系统上为 4 字节) 并且您有一个 np.uint 类型的 numpy 数组要分配(64 位上 8 个字节),您可以先使用 memoryview(temp)[:] = np.array(toobigarray, dtype=np.uint32) 进行转换,然后再使用 memcpy
    • 太棒了,谢谢!我遇到的一个问题是尝试此方法会引发错误:NotImplementedError: memoryview: unsupported format <H。这是特定于 Windows 的错误吗?有没有简单的方法绕过它?
    • @DavidHoffman:看起来在 Python 3 上,memoryviewtemp 在 little endian 机器上是显式的 little endian 字节顺序 (<H) 很挑剔,而 numpy array是本机字节顺序(H,或等效的@H)。在这种情况下,它们是相同的,因此您可以通过先投射 memoryview(temp).cast('B').cast('H')[:] = np.arange(1e8, dtype = np.uint16) 来关闭 memoryview。如果发现尺寸不匹配,这隐藏错误,您可以检查memoryview(temp).format.lstrip('@=<>!') == memoryview(np.arange(dtype=np.uint16)).format.lstrip('@=<>!') 确定。
    猜你喜欢
    • 2021-09-03
    • 2014-09-15
    • 1970-01-01
    • 2016-09-28
    • 2020-02-08
    • 2012-07-17
    • 2011-11-07
    • 2015-08-24
    • 2013-08-06
    相关资源
    最近更新 更多