在python中存储2D数组的快速方法

在python中存储2D数组的快速方法,python,numpy,cython,Python,Numpy,Cython,我正在寻找一种快速的二维数组分块方法。 我在这个网站上看到了这个主题,发现下面的解决方案可以用numpy快速处理数据 编辑: 宾宁是这样的,, 如果有, array([[ 0, 0, 0, 0, 0, 0], [ 0, 144, 0, 0, 0, 0], [ 0, 0, 0, 144, 3, 0], [109, 112, 116, 121, 40, 91]]) binning的输出将是 ar

我正在寻找一种快速的二维数组分块方法。 我在这个网站上看到了这个主题,发现下面的解决方案可以用numpy快速处理数据

编辑: 宾宁是这样的,, 如果有,

array([[  0,   0,   0,   0,   0,   0],
       [  0, 144,   0,   0,   0,   0],
       [  0,   0,   0, 144,   3,   0],
       [109, 112, 116, 121,  40,  91]])
binning的输出将是

array([[144,   0,   0],
       [221, 381, 134]])
如您所见,在本例中,输出数组的每个元素都是原始数组中2x2数组的总和。我的箱子大约是50x50

如果a的形状为m,n,则形状应为 a、 重塑(m\u料仓、m//m\u料仓、n\u料仓、n//n\u料仓)


但是因为我必须使用一个大的阵列(超过1k x 1k),这在我的电脑上需要几十毫秒。有没有办法更快地完成这项工作,比如在Cython中使用C?

这可能会令人惊讶,但汇总矩阵中的一些值并不是一件容易的任务。我的回答给出了一些见解,所以我不打算重复太多细节,但要获得最佳性能,必须以最好的方式利用现代CPU上的缓存和SIMD/管道自然浮动操作

Numpy把以上所有的事情都做好了,这是很难打败的。这不是不可能的,但要想成功,必须非常熟悉低级优化,而且不应该期望有太多的改进。天真的实现根本无法打败Numpy

下面是我使用cython和numba的尝试,所有的加速都来自并行化

让我们从基线算法开始:

def bin2d(a,K):
    m_bins = a.shape[0]//K
    n_bins = a.shape[1]//K
    return a.reshape(m_bins, K, n_bins, K).sum(3).sum(1)
并衡量其性能:

import numpy as np
N,K=2000,50
a=np.arange(N*N, dtype=np.float64).reshape(N,N)
%timeit bin2d(a,K)
# 2.76 ms ± 107 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Cython:

下面是我用Cython实现的,它使用OpenMP进行并行化。为了保持简单,我在适当的位置执行求和,因此传递的数组将被更改(numpy版本的情况并非如此):

这大约快了30%。按照@max9111的建议,使用
-c=/arch:AVX512
(否则仅使用
/Ox
和MSVC编译),使单线程版本的速度提高了约20%,而并行版本的速度仅提高了约5%

Numba:

使用numba编译的算法也一样(由于clang编译器的性能更好,它通常可以打败cython),但结果比cython稍慢,但比numpy大约慢20%:

import numba as nb
@nb.njit(parallel=True)
def nb_bin2d_parallel(a, K):
    m_bins = a.shape[0]//K
    n_bins = a.shape[1]//K
    res = np.zeros((m_bins, n_bins), dtype=np.float64)

    for k in nb.prange(m_bins*n_bins):
        i = k//m_bins
        j = k%m_bins
        for y in range(i*K+1, (i+1)*K):
            for x in range(j*K, (j+1)*K):
                a[i*K, x] += a[y,x]
        s=0.0
        for x in range(j*K, (j+1)*K):
            s+=a[i*K, x]
        res[i,j] = s
    return res
现在:

a=np.arange(N*N, dtype=np.float64).reshape(N,N)
%timeit nb_bin2d_parallel(a,K)
# 1.98 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# without parallelization: 5.8 ms

简言之:我想这是有可能击败上述,但没有免费的午餐了,因为numpy做得很好。最有潜力的可能是并行化,但由于问题的内存带宽限制,它是有限的(并且可能应该像我一样使用更智能的策略-对于50*50求和,仍然可以看到创建/管理线程的开销)


Cython还有另一个更快的尝试(至少对于大约2000个大小),它不是对小的50个元素部分执行求和,而是对整行执行求和,从而减少开销(但当行大于1-2k时,可能会有更多的缓存未命中):

%%cython-a--verbose-c=/openmp--link args=/openmp-c=/arch:AVX512
将numpy作为np导入
进口赛昂
来自cython.平行进口prange
cdef外部源*:
"""
无效计算行(双*ptr,整数N,整数y\U偏移,双*out){
双*行=ptr;

对于(int y=1;y,这基本上是对@ead答案的评论

通常情况下,对对齐求和的最好方法是尽可能多地使用标量(它映射到寄存器)。此函数也不会修改输入数组,这可能是不需要的

代码和计时

@nb.njit(parallel=True,fastmath=True,cache=True)
def nb_bin2d_parallel_2(a, K):
    #There is no bounds-checking, make sure that the dimensions are OK
    assert a.shape[0]%K==0
    assert a.shape[1]%K==0

    m_bins = a.shape[0]//K
    n_bins = a.shape[1]//K
    #Works for all datatypes, but overflow especially in small integer types
    #may occur
    res = np.zeros((m_bins, n_bins), dtype=a.dtype)

    for i in nb.prange(res.shape[0]):
        for ii in range(i*K,(i+1)*K):
            for j in range(res.shape[1]):
                TMP=res[i,j]
                for jj in range(j*K,(j+1)*K):
                    TMP+=a[ii,jj]
                res[i,j]=TMP
    return res

N,K=2000,50
a=np.arange(N*N, dtype=np.float64).reshape(N,N)

#warmup (Numba compilation is on the first call)
res_1=nb_bin2d_parallel(a, K)
res_2=cy_bin2d_parallel(a,K)
res_3=bin2d(a,K)
res_4=nb_bin2d_parallel_2(a, K)

%timeit bin2d(a,K)
#2.51 ms ± 25.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_bin2d_parallel(a, K)
#1.33 ms ± 33.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit nb_bin2d_parallel_2(a, K)
#1.05 ms ± 8.96 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit cy_bin2d_parallel(a,K) #arch:AVX2
#996 µs ± 7.94 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

N,K=4000,50
a=np.arange(N*N, dtype=np.float64).reshape(N,N)

%timeit bin2d(a,K)
#10.8 ms ± 56.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_bin2d_parallel(a, K)
#5.13 ms ± 46.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_bin2d_parallel_2(a, K)
#3.99 ms ± 31.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit cy_bin2d_parallel(a,K) #arch:AVX2
#4.31 ms ± 168 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

你能提供一些输入和输出数据的示例以及你正在使用的代码吗?使用内核2x2和stride 2x2的有效卷积应该可以很快工作,但可能有更好的方法。箱子大约是50x50。如果你像在C代码中那样在第二个循环中求和到一个标量,Numba版本应该会快一点(并将结果写入数组res,并使用np.empty分配res)还有fastmath(Numba和Cython),march=native(默认情况下使用Numba,但不使用Cython)如果函数还没有完全陷入内存瓶颈,可能会有所帮助。@max9111,谢谢,从我的角度来看,期望写入全局内存和写入寄存器/局部变量都会得到优化可能是幼稚的。这一更改带来了5%。但是使用fastmath没有任何效果(正如预期的那样,因为唯一的潜力将是最后一个求和循环)。使用/arch:AVX512(它是MSVC)为Cython带来了进一步的加速。找到了一种更快的方法(仅在Numba中测试),但也许你也可以在Cython/C中获得一点性能。我将添加一个“注释”答案。没错,它比我的(旧的)更快实现起来很舒服。我想有时候我试图帮助优化器太多了——他们只是比我好。我还发布了另一种方法,比你的版本快一点——可能有办法进一步改进,但是你必须真正理解瓶颈是什么。
a=np.arange(N*N, dtype=np.float64).reshape(N,N)
%timeit nb_bin2d_parallel(a,K)
# 1.98 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# without parallelization: 5.8 ms
%%cython -a --verbose -c=/openmp --link-args=/openmp -c=/arch:AVX512
import numpy as np
import cython
from cython.parallel import prange
cdef extern from *:
    """
    void calc_bin_row(double *ptr, int N, int y_offset, double* out){
       double *row = ptr;
       for(int y=1;y<N;y++){
           row+=y_offset; //next row
           for(int x=0;x<y_offset;x++){
               ptr[x]+=row[x];
           }
       }
       double res=0.0;
       int i=0;
       int k=0;
       for(int x=0;x<y_offset;x++){//could be made faster, but is it needed?
           res+=ptr[x];
           k++;
           if(k==N){
              k=0;
              out[i]=res;
              i++;
              res=0.0;
           }
       }
    }
    """
    void calc_bin_row(double *ptr, int N, int y_offset, double* out) nogil


@cython.boundscheck(False)
@cython.wraparound(False)
def cy_bin2d_parallel_rowise(double[:, ::1] a, int K):
    cdef int y_offset = a.shape[1]
    cdef int m_bins = a.shape[0]//K
    cdef int n_bins = a.shape[1]//K
    cdef double[:,:] res = np.empty((m_bins, n_bins), dtype=np.float64)
    cdef int i,j,k
    for k in prange(0, y_offset, K, nogil=True):
        calc_bin_row(&a[k, 0], K, y_offset, &res[k//K, 0])
    return res.base
@nb.njit(parallel=True,fastmath=True,cache=True)
def nb_bin2d_parallel_2(a, K):
    #There is no bounds-checking, make sure that the dimensions are OK
    assert a.shape[0]%K==0
    assert a.shape[1]%K==0

    m_bins = a.shape[0]//K
    n_bins = a.shape[1]//K
    #Works for all datatypes, but overflow especially in small integer types
    #may occur
    res = np.zeros((m_bins, n_bins), dtype=a.dtype)

    for i in nb.prange(res.shape[0]):
        for ii in range(i*K,(i+1)*K):
            for j in range(res.shape[1]):
                TMP=res[i,j]
                for jj in range(j*K,(j+1)*K):
                    TMP+=a[ii,jj]
                res[i,j]=TMP
    return res

N,K=2000,50
a=np.arange(N*N, dtype=np.float64).reshape(N,N)

#warmup (Numba compilation is on the first call)
res_1=nb_bin2d_parallel(a, K)
res_2=cy_bin2d_parallel(a,K)
res_3=bin2d(a,K)
res_4=nb_bin2d_parallel_2(a, K)

%timeit bin2d(a,K)
#2.51 ms ± 25.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_bin2d_parallel(a, K)
#1.33 ms ± 33.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit nb_bin2d_parallel_2(a, K)
#1.05 ms ± 8.96 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit cy_bin2d_parallel(a,K) #arch:AVX2
#996 µs ± 7.94 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

N,K=4000,50
a=np.arange(N*N, dtype=np.float64).reshape(N,N)

%timeit bin2d(a,K)
#10.8 ms ± 56.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_bin2d_parallel(a, K)
#5.13 ms ± 46.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_bin2d_parallel_2(a, K)
#3.99 ms ± 31.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit cy_bin2d_parallel(a,K) #arch:AVX2
#4.31 ms ± 168 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)