Быстрый способ размещения 2D-массива в python - PullRequest
1 голос
/ 20 апреля 2020

Я ищу быстрый способ объединения 2D-массивов. Я видел этот topi c, bin 2d массив в numpy на этом сайте и нашел следующее решение с numpy может быть быстрой обработкой данных.

Редактировать: Binning is например, если у вас есть,

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]])

, вывод за счет биннинга будет

array([[144,   0,   0],
       [221, 381, 134]])

Как вы можете видеть, каждый элемент выходного массива представляет собой сумму 2x2 массивов. в исходном массиве в этом случае. В моем случае это размеры около 50x50.

Если a имеет форму m, n, то изменение формы должно иметь форму a.reshape (m_bins, m // m_bins, n_bins, n // n_bins )

Но так как мне приходится работать с большим массивом (больше 1k x 1k), на моем P C это занимает десятки миллисекунд. Есть ли способ сделать это быстрее, например, использовать C в Cython?

Ответы [ 2 ]

1 голос
/ 21 апреля 2020

Это в основном комментарий к ответу @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)
1 голос
/ 21 апреля 2020

Это может быть удивительно, но суммирование некоторых значений в матрице не простая задача. Это и это мои ответы дают некоторое представление, поэтому я не буду повторяться в деталях, но для достижения максимальной производительности необходимо использовать кэш и SIMD / pipe- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} * * * * Понимание всех вышеперечисленных правил наилучшим образом: Это не невозможно, но нужно быть очень близким с низкоуровневой оптимизацией, чтобы быть успешным - и не стоит ожидать большого улучшения. Наивная реализация вообще не сможет побить 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):

%%cython -a  -c=/openmp --link-args=/openmp -c=/arch:AVX512
import numpy as np
import cython
from cython.parallel import prange
cdef extern from *:
    """
    //assumes direct memory and row-major-order (i.e. rows are continuous)
    double calc_bin(double *ptr, int N, int y_offset){
       double *row = ptr;
       for(int y=1;y<N;y++){
           row+=y_offset; //next row
           //there is no dependencies, so the summation can be done in parallel
           for(int x=0;x<N;x++){
               ptr[x]+=row[x];
           }
       }
       double res=0.0;
       for(int x=0;x<N;x++){
           //could be made slightly faster (summation is not in parallel), but is it needed?
           res+=ptr[x];
       }
       return res;
    }
    """
    double calc_bin(double *ptr, int N, int y_offset) nogil


@cython.boundscheck(False)
@cython.wraparound(False)
def cy_bin2d_parallel(double[:, ::1] a, int K):
    cdef int y_offset = a.shape[0]
    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(m_bins*n_bins, nogil=True):
        i = k//m_bins
        j = k%m_bins
        res[i,j] = calc_bin(&a[i*K, j*K], K, y_offset)
    return res.base

А теперь (после проверки, что результаты то же самое):

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

, что примерно на 30% быстрее. Использование -c=/arch:AVX512 (иначе скомпилировано только с /Ox с MSV C), как предложено @ max9111, сделало однопоточную версию примерно на 20% быстрее, но принесло только около 5% для параллельной версии.

Numba:

Существует тот же алгоритм, скомпилированный с Numba (который часто может побить Cython из-за лучшей производительности компилятора clang) - но результат немного медленнее, чем 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 можно по-прежнему видеть издержки для создание / управление потоками).


Существует еще одна более быстрая (по крайней мере, для размеров около 2000) попытка с Cython, которая выполняет суммирование не для небольших частей из 50 элементов, а для всего строка, таким образом, уменьшая издержки (но может иметь больше пропусков кэша, когда строка больше чем 1-2 КБ):

%%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

, который уже быстрее однопоточный (2 мс) и около 60% (1,27 мс ± 50,8 мкс на л oop (среднее ± стандартное отклонение из 7 циклов, по 100 циклов в каждом)) быстрее с распараллеливанием.

...