使用 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
模块,并评估不同的进程间通信技术。我编写了一个基准,比较了Pipe
、Queue
和Array
(均来自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 之间的性能差异,这似乎更令人期待。
【问题讨论】:
Value
和 Array
类型依赖于 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
启动新进程的方式。这可以通过三种方式完成:使用spawn
、fork
或forkserver
。 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 之间的性能比。有两个函数调用需要很长时间:Array
和sharedctypes.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 多进程进行基于websocket 的实时视频流处理