Python 对面向队列的函数使用多处理后没有性能提升

Python 对面向队列的函数使用多处理后没有性能提升,python,performance,optimization,queue,multiprocessing,Python,Performance,Optimization,Queue,Multiprocessing,我想要优化的实际代码太复杂,无法包含在这里,因此这里有一个简化的示例: def enumerate_paths(n, k): """ John want to go up a flight of stairs that has N steps. He can take up to K steps each time. This function enumerate all different ways he can go up this flight of sta

我想要优化的实际代码太复杂,无法包含在这里,因此这里有一个简化的示例:

def enumerate_paths(n, k):
    """
    John want to go up a flight of stairs that has N steps. He can take
    up to K steps each time. This function enumerate all different ways
    he can go up this flight of stairs.
    """
    paths = []
    to_analyze = [(0,)]

    while to_analyze:
        path = to_analyze.pop()
        last_step = path[-1]

        if last_step >= n:
            # John has reach the top
            paths.append(path)
            continue

        for i in range(1, k + 1):
            # possible paths from this point
            extended_path = path + (last_step + i,)
            to_analyze.append(extended_path)

    return paths
输出结果如下所示

>>> enumerate_paths(3, 2)
[(0, 2, 4), (0, 2, 3), (0, 1, 3), (0, 1, 2, 4), (0, 1, 2, 3)]
您可能会发现结果令人困惑,因此这里有一个解释。例如,
(0,1,2,4)
意味着John可以按时间顺序将脚放在第一、第二和第四步上,最后他在第四步停止,因为他只需要上3步

我试图将
多处理
合并到这段代码中,但没有观察到性能提升,甚至一点也没有

import multiprocessing

def enumerate_paths_worker(n, k, queue):
    paths = []

    while not queue.empty():
        path = queue.get()
        last_step = path[-1]

        if last_step >= n:
            # John has reach the top
            paths.append(path)
            continue

        for i in range(1, k + 1):
            # possible paths from this point
            extended_path = path + (last_step + i,)
            queue.put(extended_path)

    return paths


def enumerate_paths(n, k):
    pool = multiprocessing.Pool()
    manager = multiprocessing.Manager()
    queue = manager.Queue()

    path_init = (0,)
    queue.put(path_init)
    apply_result = pool.apply_async(enumerate_paths_worker, (n, k, queue))

    return apply_result.get()
Python列表
to_analysis
就像一个任务队列,队列中的每个项目都可以单独处理,因此我认为这个函数有可能通过使用多线程/处理来优化。另外,请注意,物品的顺序并不重要。事实上,在对其进行优化时,可以返回Python集、Numpy数组或Pandas数据帧,只要它们表示相同的路径集

奖金问题:对于这样的任务,使用Numpy、Pandas或Scipy等科学软件包可以获得多少性能?

TL;DR

如果您的实际算法不涉及比您在示例中向我们展示的更昂贵的计算,则多处理的通信开销将占主导地位,并使您的计算时间比顺序执行长很多倍


使用
apply\u async
的尝试实际上只使用了池中的一个工作线程,这就是为什么您看不出有什么不同
apply_async
只是通过设计一次为一个工人提供服务。此外,如果您的工作人员需要共享中间结果,那么仅仅将串行版本传递到池中是不够的,因此您必须修改您的目标函数以启用它

但是正如在介绍中已经说过的,如果计算量足够大,足以收回进程间通信(和进程创建)的开销,那么您的计算只会从多处理中受益

我下面针对一般问题的解决方案使用
JoinableQueue
结合进程终止的sentinel值来同步工作流。我添加了一个函数
busy\u foo
,使计算更重,以显示多处理的好处

from multiprocessing import Process
from multiprocessing import JoinableQueue as Queue
import time

SENTINEL = 'SENTINEL'

def busy_foo(x = 10e6):
    for _ in range(int(x)):
        x -= 1


def enumerate_paths(q_analyze, q_result, n, k):
    """
    John want to go up a flight of stairs that has N steps. He can take
    up to K steps each time. This function enumerate all different ways
    he can go up this flight of stairs.
    """
    for path in iter(q_analyze.get, SENTINEL):
        last_step = path[-1]

        if last_step >= n:
            busy_foo()
            # John has reach the top
            q_result.put(path)
            q_analyze.task_done()
            continue
        else:
            busy_foo()
            for i in range(1, k + 1):
                # possible paths from this point
                extended_path = path + (last_step + i,)
                q_analyze.put(extended_path)
            q_analyze.task_done()


if __name__ == '__main__':

    N_CORES = 4

    N = 6
    K = 2

    start = time.perf_counter()
    q_analyze = Queue()
    q_result = Queue()

    q_analyze.put((0,))

    pool = []
    for _ in range(N_CORES):
        pool.append(
            Process(target=enumerate_paths, args=(q_analyze, q_result, N, K))
        )

    for p in pool:
        p.start()

    q_analyze.join() # block until everything is processed

    for p in pool:
        q_analyze.put(SENTINEL)  # let the processes exit gracefully

    results = []
    while not q_result.empty():
        results.append(q_result.get())

    for p in pool:
        p.join()

    print(f'elapsed: {time.perf_counter() - start: .2f} s')
结果

如果我在注释掉上面的代码时使用了
busy\u foo
,则需要N=30,K=2(2178309个结果):

  • ~208sN_核心=4
  • 2.78s连续原稿
酸洗和解酸洗、线程在锁上运行等都是造成这种巨大差异的原因

现在,如果同时启用了
busy\u foo
和N=6,K=2(21个结果),则需要:

  • 6.45sN_芯=4
  • 30.46s连续原始
在这里,计算非常繁重,足以让开销重新获得

Numpy


Numpy可以多次加速矢量化操作,但您可能会在这一次看到Numpy的性能损失。Numpy使用连续的内存块作为它的数组。当您更改数组大小时,整个数组将不得不重新构建,这与使用python列表不同。

“但我认为它们只能处理彼此完全独立的任务”事实并非如此,例如,进程可以在列表上协同工作。对于你的奖金问题,如果不实施它,就不可能知道;可能是相同的速度,可能是1000倍faster@roganjosh我明白你的意思,但我的意思是,你不能用
map
,它基本上只是在不同的参数上同时应用函数。我实际上很难理解函数的作用。它给了我一个类似于
(0,1,2,6)
枚举路径(3,4)
的答案。这到底是什么意思?你不能在最后采取4个步骤,因为只能再采取1个步骤。您还没有实现一个上界来考虑实际可以采取多少步骤?但是,是的,现在我已经在玩代码了,我不确定它是否适合当前形式的多处理,除非您能够找到一种方法来划分这些步骤task@roganjosh我已经更新了我的帖子,加入了一个解释。基本上,John被允许“过度”,因为我认为这将使示例更加简洁(并且更容易实现)。