为啥多处理会减慢嵌套的 for 循环?

Posted

技术标签:

【中文标题】为啥多处理会减慢嵌套的 for 循环?【英文标题】:Why does multi-processing slow down a nested for loop?为什么多处理会减慢嵌套的 for 循环? 【发布时间】:2021-08-01 02:22:52 【问题描述】:

我有很多非常大的矩阵AFeatures,我正在将它们与其他一些非常大的矩阵BFeatures 进行比较,它们的形状都是(878, 2, 4, 15, 17, 512),使用欧几里得距离。我正在尝试并行化此过程以加快比较速度。 我在 Conda 环境中使用 Python 3,我的原始代码平均 100% 使用两个 CPU 内核:

    per_slice_comparisons = np.zeros(shape=(878, 878, 2, 4))
    
    for i in range(878):
        for j in range(878):
            for k in range(2):
                for l in range(4):
                    per_slice_comparisons[i, j, k, l] = np.linalg.norm(AFeatures[i, k, l, :] - BFeatures[j, k, l, :])

我尝试了两种加速代码的方法。

    使用多处理

    def fill_array(i):
        comparisons = np.zeros(shape=(878, 2, 4))
    
        for j in range(878):
            for k in range(2):
                for l in range(4):
                    comparisons[j, k, l] = np.linalg.norm(AFeatures[i, k, l, :] -BFeatures[j, k, l, :])
             comparisons[j, k, l] = 0
    
             return comparisons
    
    pool = Pool(processes=6)
    list_start_vals = range(878)
    per_slice_comparisons = np.array(pool.map(fill_array, list_start_vals))
    pool.close()
    

这种方法将运行时间增加了大约 5%,尽管所有 8 个 CPU 内核现在都以 100% 的速度使用。我尝试了许多不同的过程,越多越慢。

    这是一种稍微不同的方法,我使用 numexpr 库来执行更快的 linal.norm 操作。对于单个操作,这种方法将运行时间缩短了 10 倍。

     os.environ['NUMEXPR_MAX_THREADS'] = '8'
     os.environ['NUMEXPR_NUM_THREADS'] = '4'
     import numexpr as ne
    
     def linalg_norm(a):
         sq_norm = ne.evaluate('sum(a**2)')
         return ne.evaluate('sqrt(sq_norm)')
    
     per_slice_comparisons = np.zeros(shape=(878, 878, 2, 4))
         for i in range(878):
             for j in range(878):
                 for k in range(2):
                     for l in range(4):
                         per_slice_comparisons[i, j, k, l] = linalg_norm(AFeatures[i, k, l, :] - BFeatures[j, k, l, :])
    

但是,对于嵌套的 for 循环,这种方法将总执行时间增加了 3 倍。我不明白为什么简单地将此操作放在嵌套的 for 循环中会如此显着降低性能?如果有人对如何解决此问题有任何想法,我将不胜感激!

【问题讨论】:

【参考方案1】:

为什么多处理会减慢 Python 中的嵌套 for 循环?

创建进程是一项非常昂贵的系统操作。操作系统必须重新映射很多页面(程序、共享库、数据等),以便新创建的进程可以访问初始进程的页面。 multiprocessing 包还使用进程间通信来共享进程之间的工作。这也很慢。更不用说所需的最终连接操作了。为了高效(即尽可能减少开销),使用 multiprocessing 包的 Python 程序应该共享少量数据并执行昂贵的计算。 在您的情况下,您不需要 multiprocessing 包,因为您只使用 Numpy 数组(见下文)。

这是一种稍微不同的方法,我使用 numexpr 库来执行更快的 linal.norm 操作。对于单个操作,这种方法将运行时间缩短了 10 倍。

Numexpr 使用 threads 而不是进程,并且线程与进程相比更轻(即更便宜)。 Numexpr 还使用积极优化来尽可能地加快计算表达式的速度(CPython 不这样做)。

我不明白为什么简单地将这个操作放在嵌套的 for 循环中会如此显着降低性能?

Python 的默认实现是带有解释器的 CPython。解释器通常很慢(尤其是 CPython)。 CPython 几乎不会对您的代码进行优化。如果您想要快速循环,那么您需要将它们编译为本机代码JIT它们的替代方案。为此,您可以使用 CythonNumba。两者可以提供简单的方法来并行化您的程序。在您的情况下,使用 Numba 可能是最简单的解决方案。您可以先查看example programs。


更新:如果 Numpy 的实现是多线程的,那么多处理代码会慢很多。实际上,每个进程将在具有 N 个内核的机器上创建 N 个线程。因此将运行 N*N 个线程。这种情况称为over-subscription,并且众所周知是低效的(由于抢占式多任务处理,尤其是上下文切换)。检查这个假设的一种方法是简单地查看创建了多少线程(例如,使用 Posix 系统上的 hwloc 工具)或简单地监控处理器使用情况。

【讨论】:

感谢您的回答!我查看了您链接的页面上的示例并运行了它们,但默认多线程示例的结果在我的 conda 环境中:numpy(1 个线程)19 ms numba(1 个线程)86 ms numba(4 个线程)33 ms和外面:numpy(1个线程)92 ms numba(1个线程)78 ms numba(4个线程)25 ms 这有点令人惊讶,我的机器加速了:numpy(1 线程)61 毫秒,numba(1 线程)34 毫秒,numba(4 线程)16 毫秒。我经常使用 Numba 大大加快速度(不仅使用并行性)。它们可能是您在机器上使用的 Numba 版本的问题。如果您有空闲时间,可以向bug tracker 开票。否则,您可以尝试 Cython(到目前为止,我从未见过 Cython 代码运行速度比 CPython 慢),但我认为 Numba 更灵活且易于使用。 哈,你用的是 Anaconda!我认为 Anaconda 可以在 Numpy 调用中使用并行性,使其速度更快(更不用说由于一些英特尔软件包(如 MKL),内置插件通常得到很好的优化)。在这种情况下,“1 线程”版本实际上并不使用 1 线程。如果您使用英特尔 Anaconda 包,我认为 Numpy 在此示例中速度更快的另一个原因是英特尔 MKL 有自己非常快速的数学函数(感谢英特尔 SVML),而 Numba 可能不会在这里使用。因此,此示例可能无法代表所有情况。 是的,我也很惊讶,否则你的回答很有意义。我知道 numpy 在 Conda 上更快,但我没想到它会更快。感谢您的建议! 非常感谢!【参考方案2】:

我只是在这个问题上快速更新。我发现在计算不同的高维向量之间的欧几里得距离时,我在 Anaconda 中使用 numpy 确实得到了最好的结果。在此基础上使用多处理并没有取得任何显着的改进。

不过,我后来通过代码示例 (https://github.com/QVPR/Patch-NetVLAD) 找到了最近的 Faiss 库。 Faiss (https://anaconda.org/pytorch/faiss-gpu) 是一个用于聚类和计算不同向量之间距离的库,可用于计算余弦距离和欧几里得距离。简单地说,使用这个库可以实现的加速是巨大的——在比较大量高维矩阵时,速度远远超过 100 倍。它彻底改变了我的研究,我强烈推荐它,特别是在比较大型神经网络描述符时。

【讨论】:

以上是关于为啥多处理会减慢嵌套的 for 循环?的主要内容,如果未能解决你的问题,请参考以下文章

python中为啥我的for循环里嵌套的if只能循环一次?

为啥我的代码在执行时的初始嵌套 for 循环中进入无限循环?

同一函数中的异常处理会使编译时间减慢 2 倍以上,为啥?

为啥我在嵌套的 for 循环中分配 OpenCV Mat 对象后不包含预期值?

c语言一个死循环中为啥执行完一个功能函数就退出了?

为啥这个for循环不执行?