使用 Python 多处理进行通信的 OSX 和 Linux 之间的性能差异

Posted

技术标签:

【中文标题】使用 Python 多处理进行通信的 OSX 和 Linux 之间的性能差异【英文标题】:Performance discrepancy between OSX and Linux for communication using Python multiprocessing 【发布时间】:2018-06-02 07:33:02 【问题描述】:

我一直在尝试更多地了解 Python 的 multiprocessing 模块,并评估不同的进程间通信技术。我编写了一个基准,比较了PipeQueueArray(均来自multiprocessing)在进程之间传输numpy 数组的性能。完整的基准测试可以在here 找到。这是Queue 的测试的sn-p:

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 表现如此糟糕,我有点惊讶,因为它使用共享内存并且可能不需要酸洗,但我认为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 之间的行为非常相似。

This question 解决了 Windows 和 OSX 之间的性能差异,这似乎更令人期待。

【问题讨论】:

ValueArray 类型依赖于 Lock 来确保数据安全。获取锁是一项相当昂贵的操作,因为它需要切换到内核模式。另一方面,序列化简单的数据结构是现代 CPU 大部分时间都在做的事情,因此它的成本相当低。从 Array 中删除 Lock 应该会显示更好的性能,但您不能排除数据的竞争条件。 @noxdafox 如果您查看完整的基准代码,您会发现我实际上没有对基准的Array 部分使用锁定。即便如此,这也只能说明Array 在 Linux 上相对较差的性能,但并不一定说明 Linux 和 OSX 之间的差异。 你的 macbook 有固态硬盘,你的 linux 笔记本电脑有旋转磁盘吗? 它可以解释 Linux 中的 Array 缓慢。 Python 共享内存实现似乎是在文件系统上创建文件(请参阅***.com/questions/44747145/…)。我认为 SSD 与旋转磁盘可以解释那里的差异。不过,它并没有解释为什么管道在 mac 上这么慢。 您应该考虑测量 CPU 时间而不是挂钟时间。 【参考方案1】:

好吧,当我们用 python 谈论多进程时,会发生这些事情:

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

osx 和 linux 之间存在巨大差异。而osx是基于Unix的,处理多任务进程的方式不同于linux。

Unix 安装需要严格且定义明确的硬件机制,并且只能在特定的 CPU 机器上运行,而且可能 osx 并不是为了加速 python 进程而设计的。这个原因可能是原因。

有关更多详细信息,您可以阅读MultiProcessing 文档。

希望对你有帮助。

【讨论】:

我很想了解更多关于 OSX 和 Linux 之间的哪些差异在此处产生影响的信息。你能在这个话题上扩大你的答案吗? 我相信 OSX 和其他操作系统不是为 python 设计的。【参考方案2】:

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

使用multiprocessing 中的Array 使用C types Python library 进行C 调用以设置数组的内存。这在 Linux 上花费的时间比在 OSX 上要多。您还可以使用 pypy 在 OSX 上观察到这一点。使用 pypy(以及 GCC 和 LLVM)设置内存比在 OSX 上使用 python3(使用 Clang)花费更长的时间。

TL;DR:Windows 和 OSX 的区别在于多处理启动新进程的方式

主要区别在于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快,比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 之间的性能比。有两个函数调用需要很长时间:Arraysharedctypes.py 中的RawArray。这些函数仅在 Array 场景中调用(不在 Pipe 或 Queue 中)。在 Linux 上,这些调用占用了将近 70% 的时间,而在 OSX 上只有 42% 的时间。所以这是一个主要因素。

如果我们放大to the code,我们会看到Array(第84行)调用RawArray,而RawArray(第54行)除了调用ctypes.memset(documentation)没有什么特别之处.所以我们有一个嫌疑人。让我们测试一下。

以下代码使用 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 和 Apples LLVM 编译的,这比 Python 更类似于 Linux 世界,Python 在 OSX 上直接针对 Clang 编译。通常,Python 程序在 pypy 上的运行速度比在 CPython 上快,但上面的代码在 pypy 上的运行速度要慢 6.4 倍(在相同的硬件上!)。

我对 C 工具链和 C 库的了解有限,因此无法深入挖掘。所以我的结论是:OSX 和 Windows 使用 Array 更快,因为对 C 库的内存调用会减慢 Linux 上的 Array

分析 OSX - Windows 性能差异

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

结果如下:

OSX: 数组:10.895 秒内调用 225668 次 管道:209552 次调用在 6.894 秒内 队列:728173 次呼叫在 7.892 秒内 窗口: 数组:296.050 秒内调用 354076 次 管道:234.996 秒内调用 374229 次 队列:903705 次呼叫在 250.966 秒内

我们可以看到:

    Windows 实现(使用 spawn)比 OSX(使用 fork)需要更多的调用; Windows 实现每次调用所花费的时间比 OSX 多得多。

不是很明显,但需要注意的是,如果您查看每次调用的平均时间,三种多处理方法(数组、队列和管道)之间的相对模式是相同的(见下图)。换句话说: Array、Queue 和 Pipe 在 OSX 和 Windows 中的性能差异完全可以用两个因素来解释: 1. 两个平台在 Python 性能上的差异; 2. 两个平台处理多处理的不同方式。

换句话说:调用次数的差异由multiprocessing 文档的Contexts and start methods 部分解释。执行时间的差异在 OSX 和 Windows 之间的 Python 性能差异中进行了解释。如果排除这两个组件,Array、Queue 和 Pipe 的相对性能在 OSX 和 Windows 上(或多或少)具有可比性,如下图所示。

【讨论】:

综合回答,但问题与 Windows 无关...... OP 询问了 Mac 和 Linux 之间的区别。 @CoreyGoldberg:哎呀……该死。太愚蠢了……我也在 Linux 上运行它。将在几个小时内添加... @CoreyGoldberg 添加了使用 Array 对 OSX 与 Linux 的分析。 @agtoever 感谢您的详细分析。所以为了进一步提炼你的结果,你是说它基本上归结为ctypes.memset 在这些平台上的性能差异?我不知道为什么会这样。我想知道memset 在这些平台上的纯C 代码中的相对性能如何?

以上是关于使用 Python 多处理进行通信的 OSX 和 Linux 之间的性能差异的主要内容,如果未能解决你的问题,请参考以下文章

Python 子进程、通信和多处理/多线程

如何结合python多处理和管道技术?

使用python 多进程进行基于websocket 的实时视频流处理

python 多线程处理

使用python 多进程进行基于websocket 的实时视频流处理

Python 多进程内存占用问题