Chociaż prośba dotyczy numpy
rozwiązania, postanowiłem sprawdzić, czy istnieje numba
rozwiązanie oparte na ciekawych rozwiązaniach. I rzeczywiście jest! Oto podejście, które reprezentuje podzieloną na partycje listę jako poszarpaną tablicę przechowywaną w pojedynczym wstępnie przydzielonym buforze. Inspiruje to argsort
podejście zaproponowane przez Paula Panzera . (W przypadku starszej wersji, która nie działała tak dobrze, ale była prostsza, patrz poniżej).
@numba.jit(numba.void(numba.int64[:],
numba.int64[:],
numba.int64[:]),
nopython=True)
def enum_bins_numba_buffer_inner(ints, bins, starts):
for x in range(len(ints)):
i = ints[x]
bins[starts[i]] = x
starts[i] += 1
@numba.jit(nopython=False) # Not 100% sure this does anything...
def enum_bins_numba_buffer(ints):
ends = np.bincount(ints).cumsum()
starts = np.empty(ends.shape, dtype=np.int64)
starts[1:] = ends[:-1]
starts[0] = 0
bins = np.empty(ints.shape, dtype=np.int64)
enum_bins_numba_buffer_inner(ints, bins, starts)
starts[1:] = ends[:-1]
starts[0] = 0
return [bins[s:e] for s, e in zip(starts, ends)]
Przetwarza dziesięciomilionową listę elementów w 75 ms, co stanowi prawie 50-krotne przyspieszenie w porównaniu z wersją opartą na listach napisaną w czystym języku Python.
W przypadku wolniejszej, ale nieco bardziej czytelnej wersji, oto co miałem wcześniej, w oparciu o niedawno dodane eksperymentalne wsparcie dla dynamicznie zmieniających się „list maszynowych”, które pozwalają nam szybciej zapełniać każdy pojemnik w niewłaściwym porządku.
To numba
trochę zmaga się z silnikiem wnioskowania typu i jestem pewien, że jest lepszy sposób na poradzenie sobie z tą częścią. To również okazuje się prawie 10 razy wolniejsze niż powyższe.
@numba.jit(nopython=True)
def enum_bins_numba(ints):
bins = numba.typed.List()
for i in range(ints.max() + 1):
inner = numba.typed.List()
inner.append(0) # An awkward way of forcing type inference.
inner.pop()
bins.append(inner)
for x, i in enumerate(ints):
bins[i].append(x)
return bins
Przetestowałem je pod kątem następujących elementów:
def enum_bins_dict(ints):
enum_bins = defaultdict(list)
for k, v in enumerate(ints):
enum_bins[v].append(k)
return enum_bins
def enum_bins_list(ints):
enum_bins = [[] for i in range(ints.max() + 1)]
for x, i in enumerate(ints):
enum_bins[i].append(x)
return enum_bins
def enum_bins_sparse(ints):
M, N = ints.max() + 1, ints.size
return sparse.csc_matrix((ints, ints, np.arange(N + 1)),
(M, N)).tolil().rows.tolist()
Przetestowałem je również w stosunku do wstępnie skompilowanej wersji cytonu podobnej do enum_bins_numba_buffer
(opisanej szczegółowo poniżej).
Na liście dziesięciu milionów losowych liczb całkowitych ( ints = np.random.randint(0, 100, 10000000)
) otrzymuję następujące wyniki:
enum_bins_dict(ints)
3.71 s ± 80.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_list(ints)
3.28 s ± 52.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_sparse(ints)
1.02 s ± 34.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_numba(ints)
693 ms ± 5.81 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_cython(ints)
82.3 ms ± 1.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
enum_bins_numba_buffer(ints)
77.4 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Imponująco, ten sposób pracy z numba
programem przewyższa cython
wersję tej samej funkcji, nawet przy wyłączonym sprawdzaniu granic. Nie mam jeszcze wystarczającej znajomości, pythran
aby przetestować to podejście przy użyciu tej metody, ale chciałbym zobaczyć porównanie. Wydaje się prawdopodobne, na podstawie tego przyspieszenia, że ta pythran
wersja może być nieco szybsza.
Oto cython
wersja w celach informacyjnych z kilkoma instrukcjami kompilacji. Po cython
zainstalowaniu będziesz potrzebować prostego setup.py
pliku takiego jak ten:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import numpy
ext_modules = [
Extension(
'enum_bins_cython',
['enum_bins_cython.pyx'],
)
]
setup(
ext_modules=cythonize(ext_modules),
include_dirs=[numpy.get_include()]
)
I moduł cytonowy enum_bins_cython.pyx
:
# cython: language_level=3
import cython
import numpy
cimport numpy
@cython.boundscheck(False)
@cython.cdivision(True)
@cython.wraparound(False)
cdef void enum_bins_inner(long[:] ints, long[:] bins, long[:] starts) nogil:
cdef long i, x
for x in range(len(ints)):
i = ints[x]
bins[starts[i]] = x
starts[i] = starts[i] + 1
def enum_bins_cython(ints):
assert (ints >= 0).all()
# There might be a way to avoid storing two offset arrays and
# save memory, but `enum_bins_inner` modifies the input, and
# having separate lists of starts and ends is convenient for
# the final partition stage.
ends = numpy.bincount(ints).cumsum()
starts = numpy.empty(ends.shape, dtype=numpy.int64)
starts[1:] = ends[:-1]
starts[0] = 0
bins = numpy.empty(ints.shape, dtype=numpy.int64)
enum_bins_inner(ints, bins, starts)
starts[1:] = ends[:-1]
starts[0] = 0
return [bins[s:e] for s, e in zip(starts, ends)]
Z tymi dwoma plikami w katalogu roboczym uruchom następującą komendę:
python setup.py build_ext --inplace
Następnie możesz zaimportować funkcję za pomocą from enum_bins_cython import enum_bins_cython
.
np.argsort([1, 2, 2, 0, 0, 1, 3, 5])
dajearray([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64)
. wtedy możesz po prostu porównać kolejne elementy.