使用Python多进程通信时,OSX和Linux之间的性能差异问题

26

我一直在尝试学习有关Python的multiprocessing模块,并评估进程间通信的不同技术。我编写了一个基准测试,比较了PipeQueueArray(都来自multiprocessing)在进程之间传输numpy数组的性能。完整的基准测试可以在这里找到。这是一个关于Queue的测试代码片段:

def process_with_queue(input_queue, output_queue):
    source = input_queue.get()
    dest = source**2
    output_queue.put(dest)


def test_with_queue(size):

    source = np.random.random(size)

    input_queue = Queue()
    output_queue = Queue()

    p = Process(target=process_with_queue, args=(input_queue, output_queue))
    start = timer()
    p.start()
    input_queue.put(source)
    result = output_queue.get()
    end = timer()

    np.testing.assert_allclose(source**2, result)

    return end - start

我在我的Linux笔记本上运行了这个测试,并得到了数组大小为1000000时的以下结果:

Using mp.Array: time for 20 iters: total=2.4869s, avg=0.12435s
Using mp.Queue: time for 20 iters: total=0.6583s, avg=0.032915s
Using mp.Pipe:  time for 20 iters: total=0.63691s, avg=0.031845s

看到Array的表现如此糟糕,我有些惊讶,因为它使用共享内存,理论上不需要进行pickling,但我认为在numpy中可能存在一些我无法控制的复制。

然而,我在Macbook上运行了相同的测试(数组大小仍为1000000),得到了以下结果:

Using mp.Array: time for 20 iters: total=1.6917s, avg=0.084587s
Using mp.Queue: time for 20 iters: total=2.3478s, avg=0.11739s
Using mp.Pipe:  time for 20 iters: total=8.7709s, avg=0.43855s

实际的时间差异并不令人惊讶,因为不同的系统表现出不同的性能。真正令人惊讶的是相对时间的差异。

这是什么原因造成的?对我来说,这是一个非常令人惊讶的结果。我不会对Linux和Windows之间,或OSX和Windows之间的显著差异感到惊讶,但我认为在OSX和Linux之间,它们的行为应该非常相似。

这个问题涉及到Windows和OSX之间的性能差异,这似乎更容易理解。


ValueArray类型依赖于Lock来确保数据安全。获取锁是一项相当昂贵的操作,因为它需要切换到内核模式。另一方面,序列化简单数据结构是现代CPU大部分时间所做的事情,因此其成本相当低。从Array中删除Lock应该会显示更好的性能,但您不能排除数据上的竞争条件。 - noxdafox
@noxdafox 如果你查看完整的基准测试代码,你会发现我实际上没有在基准测试的 Array 部分使用锁。即使如此,在Linux上相对性能较差的 Array 只能解释一部分,但并不一定能解释Linux和OSX之间的差异。 - ddavella
1
你的MacBook有固态硬盘,而你的Linux笔记本电脑有旋转硬盘吗? - Hannu
2
它可以解释在Linux中数组速度慢的原因。Python共享内存实现似乎会在文件系统上创建文件(见https://stackoverflow.com/questions/44747145/writing-to-shared-memory-in-python-is-very-slow)。 我会认为SSD与旋转磁盘的区别会解释其中的差异。但这并不解释为什么管道在Mac上如此缓慢。 - Hannu
2
你应该考虑测量 CPU 时间而不是墙钟时间。 - csl
显示剩余3条评论
2个回答

7

TL;DR: 在OSX上,使用Array更快,因为在Linux上调用C库会使Array变慢。

使用multiprocessing中的Array使用C类型Python库来进行C调用以设置Array的内存。这在Linux上比在OSX上花费的时间相对较长。您还可以通过使用pypy在OSX上观察到这一点。使用pypy(和GCC和LLVM)设置内存所需的时间比在OSX上使用python3(使用Clang)要长得多。

TL;DR: Windows和OSX之间的差异在于multiprocessing启动新进程的方式。

主要区别在于multiprocessing的实现方式,在OSX和Windows下工作方式不同。最重要的区别在于multiprocessing启动新进程的方式。有三种方法可以做到这一点:使用spawnforkforkserver。在Windows下,默认(也是唯一支持的)方式是spawn。在*nix(包括OSX)下,默认方式是fork。这在multiprocessing文档的Contexts and start methods部分有说明。
另一个导致结果差异的原因是您执行的迭代次数较少。
如果增加迭代次数并计算每个时间单位处理的函数调用次数,则在三种方法之间得到相对一致的结果。
进一步分析:使用cProfile查看函数调用
我删除了您的timeit计时器函数,并将代码包装在cProfile分析器中。
我添加了这个包装函数:
def run_test(iters, size, func):
    for _ in range(iters):
        func(size)

我用以下代码替换了main()中的循环:

for func in [test_with_array, test_with_pipe, test_with_queue]:
    print(f"*** Running {func.__name__} ***")
    pr = cProfile.Profile()
    pr.enable()
    run_test(args.iters, args.size, func)
    pr.disable()
    ps = pstats.Stats(pr, stream=sys.stdout)
    ps.strip_dirs().sort_stats('cumtime').print_stats()

分析OSX和Linux之间使用Array的差异

我的观察是,Queue比Pipe快,Pipe比Array快。无论在哪个平台(OSX / Linux / Windows),Queue的速度都比Pipe快2到3倍。在OSX和Windows上,Pipe比Array快大约1.2到1.5倍。但是在Linux上,Pipe比Array快大约3.6倍。换句话说,在Linux上,Array相对于Windows和OSX而言要慢得多。这很奇怪。

使用cProfile数据,我比较了OSX和Linux之间的性能比率。有两个函数调用需要花费很长时间:sharedctypes.py中的ArrayRawArray。这些函数仅在Array场景中调用(不在Pipe或Queue中)。在Linux上,这些调用占用了近70%的时间,而在OSX上只占用了42%的时间。因此,这是一个重要因素。

如果我们放大代码, 我们会看到Array (第84行) 调用了 RawArray, 而 RawArray (第54行) 没有什么特别之处, 只是调用了 ctypes.memset (文档). 所以我们有一个嫌疑人。让我们测试一下。
以下代码使用 timeit 来测试将 1 MB 内存缓冲区设置为 'A' 的性能。
import timeit
cmds = """\
import ctypes
s=ctypes.create_string_buffer(1024*1024)
ctypes.memset(ctypes.addressof(s), 65, ctypes.sizeof(s))"""
timeit.timeit(cmds, number=100000)

在我的MacBookPro和Linux服务器上运行此代码,确认该代码在Linux上的运行速度比OSX慢得多。我知道pypy是在OSX上使用GCC和苹果LLVM编译的,这更类似于Linux世界而不是Python,因为Python在OSX上直接针对Clang进行了编译。通常来说,Python程序在pypy上的运行速度比在CPython上快,但是上述代码在pypy上运行得慢了6.4倍(在相同的硬件上)。
我的C工具链和C库知识有限,所以我无法深入挖掘。因此,我的结论是:由于内存调用C库会使Array在Linux上减速,因此OSX和Windows在Array方面更快

OSX和Windows性能差异的分析

接下来我在我的双系统MacBook Pro上在OSX和Windows下运行了这个程序。优点是底层硬件相同,只有操作系统不同。我将迭代次数增加到1000,大小增加到10000。

结果如下:

  • OSX:
    • 数组:10.895秒内调用225668次
    • 管道:6.894秒内调用209552次
    • 队列:7.892秒内调用728173次
  • Windows:
    • 数组:296.050秒内调用354076次
    • 管道:234.996秒内调用374229次
    • 队列:250.966秒内调用903705次

我们可以看到:

  1. Windows实现(使用spawn)的调用次数比OSX(使用fork)多;
  2. Windows实现每次调用的时间比OSX长得多。
请注意,如果您查看每个调用的平均时间,三种多进程方法(Array、Queue和Pipe)之间的相对模式是相同的(见下面的图表),这一点并不立即明显,但值得注意。换句话说,OSX和Windows中Array、Queue和Pipe之间的性能差异可以完全解释为两个因素:1.两个平台之间Python性能的差异;2.两个平台处理多进程的不同方式。换句话说,调用数量的差异在Contexts and start methods部分的文档中有所解释。执行时间的差异在于OSX和Windows之间Python性能的差异。如果排除这两个因素,Array、Queue和Pipe的相对性能在OSX和Windows上(或多或少)是可比较的,如下面的图表所示。

Performance differences of Array, Queue and Pipe between OSX and Windows


回答很全面,但问题不是关于Windows的... OP问的是Mac和Linux之间的区别。 - Corey Goldberg
@CoreyGoldberg:哦...该死。这太蠢了...我也在Linux上运行了它。几个小时后会添加进去... - agtoever
@CoreyGoldberg 添加了使用数组对OSX和Linux进行比较分析的内容。 - agtoever
@agtoever,非常感谢您提供如此详细的分析。那么,为了进一步概括您的结果,您是说这基本上归结为ctypes.memset在这些平台上性能差异吗?我不知道为什么会出现这种情况。我想知道在这些平台上纯C代码中memset的相对性能如何? - ddavella

-4

当我们谈论使用Python进行多进程时,会发生以下事情:

  • 操作系统完成所有的多任务工作
  • 多核并发的唯一选择
  • 系统资源的重复使用

OSX和Linux之间存在巨大的差异。OSX基于Unix,并以不同于Linux的方式处理多任务处理。

Unix安装需要严格定义的硬件设备,并且仅适用于特定的CPU机器,也许OSX并不是为加速Python进程而设计的。这可能是原因。

有关更多详细信息,请阅读MultiProcessing文档。

希望能对您有所帮助。


4
我很乐意了解在这里产生影响的OSX和Linux之间的不同之处。你能否在这个话题上扩展一下你的回答? - William Payne
我认为OSX和其他操作系统并不是为Python设计的。 - RedEyed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接