Python 多处理调用在以后的调用中变慢

Python multiprocessing calls become slower for later calls

我正在尝试通过在参数列表上调用某些函数来处理大量作业负载:

import multiprocessing as mp 

print("Number of processors: ", mp.cpu_count())

pool = mp.Pool(mp.cpu_count()) 

try:
     results = pool.map_async(consume_one, [list-of-arguments]).get()
except:
     print(e) 
finally:
     pool.close()

对于 consume_one() 的每次调用,我们从“[参数列表]”传递一个值,并在该函数中记录函数 consume_one() 的开始和结束时间。

观测值如下:

Completed processing for ... in 0:03:34.283025
Completed processing for ... in 0:04:24.109049
Completed processing for ... in 0:04:58.464374
Completed processing for ... in 0:05:11.830404
Completed processing for ... in 0:08:32.234539
Completed processing for ... in 0:09:09.725937
Completed processing for ... in 0:09:10.968685
Completed processing for ... in 0:09:51.642501
Completed processing for ... in 0:10:58.076675
Completed processing for ... in 0:12:30.905190
Completed processing for ... in 0:14:01.051716

正如我们在日志中看到的那样,对同一函数的所有后续调用花费的时间越来越长,但这并不是因为这些调用的参数,而且所有这些调用的参数或多或少都相同.

我的问题是:

为什么会发生这种情况?
我该如何调试?

Q1 :
"Why this might be happening?"

A1 :
累积处理效率低下的增长

Q2 :
" How can I debug this?"

A2 :
最好通过了解你失去大部分处理效率的组成。首先原则上,然后在实际技术中使用,如何减少或更好地消除这种昂贵的开销热点。

首先找出处理效率低下的个别根本原因:

  • Python 解释器进程产生的成本 add-on 间接费用(在一些 O/S-es 完整的情况下,我重复完整的副本 - 即可能是一些 [GB] 移动 RAM-to-RAM 物理 memory-I/O 硬件瓶颈 - 检查 (a) [=109] 有多少 I/O 通道=]在从/到a的路上physical-RAM和(b)是否复制了这么多process-memory-allocations实际上没有转O/Svirtual-memory 管理到 swapping-mode,即由于 (processes-times-replicated)-内存的范围不适合您的计算平台的 physical-RAM 占用空间,O/S virtual-memory 管理器开始通过交换 large-chunks 来“模拟”这样一个缺失的 RAM 来满足内存访问需求 on-demand(参见 LRU-policy 等) process-reserved physical-RAM 数据输出(一旦 LRU-policy 这么说),到磁盘上(这里看到 ~ 1E5+ 更大的 data-access 延迟并想象另一个 add-on 成本从 traffic-jamming,如果不是直接阻塞,任何其他 physical-RAM 到 CPU data-flow,在移动到那里交换的巨大块 [GB]-s to/from这么慢swap-spacedisk-storage。然后将此成本乘以 ~ 2X 系数,作为将另一个数据块从慢速 swap-space disk-storage 移回现在“已释放”-RAM 的成本,用于下一个允许反过来使用它的过程(至少对于某些 fair-amount-of-time,因为 O/S virtual-memory 经理认为这是公平的),与之前 [=134= 的成本基本相同] 将 RAM-data 的方块移出(为这个 space 移动)。有点“SOKOBAN”问题,当移动所有数以亿计的 [GB]-s 但都只通过一对 CPU-to-physical-RAM memory-I/O 通道时,所以经常等待对于任何下一个 filesystem-block 慢 1E5+ 倍 disk-storage { read |在漫长的等待队列中写入 } - 它是如此残酷,正如我们在这里谈论由此产生的 end-to-end 计算策略效率,它通常被称为 RAM-thrashingswap-juggling 在高效计算科学中的表现 anti-pattern )

  • Python 解释器 process-to-process data-transfer 的成本( SER/xfer/DES 发生在传递的 call-signature 参数和返回的结果中) - this 是一个示例,其中这种性能 anti-pattern 基本上可以避免,从而节省 [=16] 中不必要的 SER/xfer/DES data-flow 的所有 add-on 成本=] (!太字节的 physical-RAM memory-I/O,CPU-pickle.dumps(),O/S-pipe-xfers,CPU-pickle。 loads() 和另一个 physical-RAM memory-I/O-s——所有浪费在 ~ 75 [TB] data-flow 上的东西,由于错误使用了“slim”-SLOC list-comprehension 基于“外部”迭代器,类似于上面使用的 .map_async() 方法中的“隐藏”迭代器)

  • 您的代码效率低下的成本不 re-using 昂贵 pre-fetched cache-lines

    中的数据
  • 硬件热节流的成本-促进CPU频率,一旦核心开始做一些繁重的工作并变暖(节流可以在某些硬件上推迟,如果一项工作可以转移到另一个更酷的 CPU-core - 但代价是丢失 core-local 缓存数据,因此代码必须以 TimeDOMAIN 中最昂贵的价格再次 re-fetch 它,就像它被移到冷却器上一样,但工作频率更高,CPU-core)。在这种情况下,您的代码产生了大量的进程(更不用说 O/S-process-scheduler add-on 现在的成本),没有更酷的 CPU-core 了,您的硬件诉诸热节流 - 即工作在越来越低的 CPU-core 频率

  • 显式不受控制的垃圾回收的成本是其自身重要性的一章

有关优缺点和开销测试模板的详细论证,您可能会喜欢 this collection of multiprocessing.Pool() & joblib.Parallel() related

如果您不仅对调试问题感兴趣,而且对解决 end-to-end process-performance 感兴趣,请尝试 re-factor 代码以从根本上防止这种情况发生 -如果你确定你想要那么多进程(虽然这些很可能是 memory-starved,等待尚未从慢速和 overloaded/blocked physical-RAM-I/O 通道获取数据),是确保从 __main__-Python 解释器托管的“外部”-item-iterator 转移到 block-based 工作。在那里,您命令工作进程“内部”迭代(避免 SER/xfer/DES add-on 成本的所有非常多的重复)超过他们命令的、分离的 block-of-list,由 __main__ 分区-Python 解释器。由于 Python 解释器 process-instantiation 以已知方式工作(复制 state-full 副本 o__main__-Python 解释器 - 以前尝试减少由 O/S 复制的数据量 - 提供的“分叉”已被证明会导致问题甚至可能导致死锁情况,因此如果代码必须足够健壮,如果它要投入生产或提供某种关键智能),这里要注意,列表 per-se 将已经存在于工作进程中,即可以通过命令 index-ranges 迭代它们的“私有”部分(此 list 的分离块之一),即无需将任何其他参数昂贵地传递给 .Pool()-进程,但是相应的 index-range ~ 与 ( startHere, stopHere ) 元组一样微不足道,即映射/覆盖整个列表。返回结果的成本取决于这些结果的组成。存在可以有效执行此操作的工具,无论是在 block-transfer 进程之间 list-block 完成,还是在压缩文件 - I/O 存储中。如果性能是目标,细节一如既往地重要。由于项目在 consume_one() 处理的 as-is 状态下需要大约 3 分钟以上的时间,因此以整体处理效率和性能的名义,有很多机会可以加快速度。

Block-based“内部”-迭代可以继续调用 as-isconsume_one(),如果某些性能改进工具不能使它变得更快 - 是 numpy-矢量化(使用内部高性能多核、高速缓存 re-use 高效 bare-metal 优化库)或 numba-JIT-compiled 加速处理(作为 re-use 计数 JIT/LLVM-compiled 此处的代码朝着您的方向工作,以提高整体性能)