2

我有很多非常大的矩阵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, :])

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

  1. 使用多处理

    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% 的速度使用。我尝试了许多不同的过程,越多越慢。

  1. 这是一种稍微不同的方法,我使用 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 循环中会如此显着降低性能?如果有人对如何解决此问题有任何想法,我将不胜感激!

4

2 回答 2

3

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

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

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

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

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

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


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

于 2021-05-10T22:41:48.413 回答
1

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

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

于 2021-08-20T18:15:08.420 回答