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

dda*_*lla 26 python linux macos communication multiprocessing

我一直在尝试更多地了解Python的multiprocessing模块,并评估不同的流程之间的通信技术.我写的比较的性能的基准Pipe,QueueArray(都来自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
Run Code Online (Sandbox Code Playgroud)

我在我的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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

真正的时间差异并不令人惊讶,因为当然不同的系统会表现出不同的性能.什么如此令人惊讶的是在相对时间的差异.

有什么可以解释这个?这对我来说是一个非常令人惊讶的结果.看到Linux和Windows,OSX和Windows之间存在如此明显的差异,我不会感到惊讶,但我有点认为这些东西在OSX和Linux之间的表现非常相似.

这个问题解决了Windows和OSX之间的性能差异,这似乎更令人期待.

agt*_*ver 5

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

使用Arrayfrommultiprocessing使用C 类型 Python 库进行 C 调用以设置数组的内存。这在 Linux 上比在 OSX 上花费的时间相对更多。您还可以使用 pypy 在 OSX 上观察这一点。使用 pypy(以及 GCC 和 LLVM)设置内存比在 OSX 上使用 python3(使用 Clang)花费的时间要长得多。

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

主要区别在于 的实现multiprocessing,它在 OSX 和 Windows 下的工作方式不同。最重要的区别是multiprocessing启动新流程的方式。可以通过三种方式完成此操作:使用spawn,forkforkserver。Windows 下的默认(且仅受支持)方式是spawn. *nix(包括 OSX)下的默认方式是fork. 这记录在文档的上下文和启动方法部分multiprocessing

结果偏差的另一个原因是您采用的迭代次数较少。

如果增加迭代次数并计算每个时间单位处理的函数调用次数,则三种方法之间会得到相对一致的结果。

进一步分析:用cProfile看函数调用

我删除了您的timeit计时器功能并将您的代码包装在cProfile分析器中。

我添加了这个包装函数:

def run_test(iters, size, func):
    for _ in range(iters):
        func(size)
Run Code Online (Sandbox Code Playgroud)

我用以下方法替换了循环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()
Run Code Online (Sandbox Code Playgroud)

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

如果我们放大代码,我们会看到Array(第 84 行)调用RawArrayRawArray(第 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)
Run Code Online (Sandbox Code Playgroud)

在我的 MacBookPro 和我的 Linux 服务器上运行它证实了它在 Linux 上的运行速度比在 OSX 上慢得多的行为。知道pypy在使用GCC和 Apples LLVM编译的 OSX 上,这比 Python 更类似于 Linux 世界,Python 在 OSX 上直接针对Clang编译。通常,Python 程序在 pypy 上的运行速度比在 CPython 上快,但上面的代码在 pypy 上的运行速度要慢 6.4 倍(在相同的硬件上!)。

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

OSX-Windows性能差异分析

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

结果如下:

  • 操作系统:
    • 数组:10.895 秒内 225668 次调用
    • 管道:6.894 秒内 209552 次调用
    • 队列:728173 个呼叫在 7.892 秒内
  • 视窗:
    • 数组:296.050 秒内 354076 次调用
    • 管道:234.996 秒内 374229 次调用
    • 队列:903705 个呼叫在 250.966 秒内

我们可以看到:

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

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

换句话说:文档的上下文和启动方法部分解释了调用次数的差异multiprocessing。OSX 和 Windows 之间 Python 的性能差异解释了执行时间的差异。如果去掉这两个组件,数组、队列和管道的相对性能在 OSX 和 Windows 上(或多或少)相当,如下图所示。

OSX 和 Windows 之间数组、队列和管道的性能差异