在python中存储2D数组的快速方法
我正在寻找一种快速的二维数组分块方法。 我在这个网站上看到了这个主题,发现下面的解决方案可以用numpy快速处理数据 编辑: 宾宁是这样的,, 如果有,在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
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)