NumPy: функція для одночасних max () та min ()


109

numpy.amax () знайде максимальне значення в масиві, а numpy.amin () робить те ж саме для значення min. Якщо я хочу знайти і max, і min, я повинен викликати обидві функції, що вимагає переходу через (дуже великий) масив двічі, що здається повільним.

Чи є в numpy API функція, яка знаходить і max, і min, лише за один прохід через дані?


1
Наскільки великий дуже великий? Якщо я отримаю трохи часу, я проведу кілька тестів, порівнюючи реалізацію amaxamin
fortran

1
Я визнаю, "дуже великий" є суб'єктивним. У моєму випадку я говорю про масиви, які є кількома ГБ.
Стюарт Берг

це досить велике. Я зашифрував приклад для його обчислення у фортран (навіть якщо ви не знаєте фортран, це має бути досить легко зрозуміти код). Це дійсно має велике значення, запускаючи його від fortran проти перебігу через numpy. (Імовірно, ви повинні мати змогу отримати таку саму продуктивність від C ...) Я не впевнений - я гадаю, що нам знадобиться кумедний розробник, щоб прокоментувати, чому мої функції виконують набагато краще, ніж їх ...
mgilson

Звичайно, це навряд чи новаторська ідея. Наприклад, бібліотека boost minmax (C ++) забезпечує реалізацію алгоритму, який я шукаю.
Стюарт Берг

3
Насправді не відповідь на поставлене запитання, але, напевно, цікавить людей з цієї теми. Запитував NumPy про додавання minmaxдо випускової бібліотеки ( github.com/numpy/numpy/isissue/9836 ).
jakirkham

Відповіді:


49

Чи є в numpy API функція, яка знаходить і max, і min, лише за один прохід через дані?

Ні. На момент написання цього тексту такої функції немає. (І так, якби була така функція, її ефективність була б значно кращою, ніж дзвінки numpy.amin()та numpy.amax()послідовно на великому масиві.)


31

Я не думаю, що проходження масиву два рази є проблемою. Розглянемо наступний псевдо-код:

minval = array[0]
maxval = array[0]
for i in array:
    if i < minval:
       minval = i
    if i > maxval:
       maxval = i

Поки тут лише 1 цикл, ще є дві перевірки. (Замість того, щоб було 2 петлі з 1 перевіркою). Дійсно єдине, що ви економите, - це накладні витрати на 1 петлю. Якщо ви, як кажете, масиви дійсно великі, цей накладний малий порівняно з фактичним навантаженням циклу. (Зверніть увагу, що це все реалізовано на C, тому петлі все одно є більш-менш вільними).


РЕДАКЦІЯ Вибачте за 4 з вас, хто підтримував і вірив у мене. Ви точно можете оптимізувати це.

Ось код коду fortran, який можна скласти в модуль python через f2py(можливо, Cythonгуру може підійти і порівняти це з оптимізованою версією C ...):

subroutine minmax1(a,n,amin,amax)
  implicit none
  !f2py intent(hidden) :: n
  !f2py intent(out) :: amin,amax
  !f2py intent(in) :: a
  integer n
  real a(n),amin,amax
  integer i

  amin = a(1)
  amax = a(1)
  do i=2, n
     if(a(i) > amax)then
        amax = a(i)
     elseif(a(i) < amin) then
        amin = a(i)
     endif
  enddo
end subroutine minmax1

subroutine minmax2(a,n,amin,amax)
  implicit none
  !f2py intent(hidden) :: n
  !f2py intent(out) :: amin,amax
  !f2py intent(in) :: a
  integer n
  real a(n),amin,amax
  amin = minval(a)
  amax = maxval(a)
end subroutine minmax2

Складіть його через:

f2py -m untitled -c fortran_code.f90

А тепер ми знаходимось у місці, де ми можемо перевірити це:

import timeit

size = 100000
repeat = 10000

print timeit.timeit(
    'np.min(a); np.max(a)',
    setup='import numpy as np; a = np.arange(%d, dtype=np.float32)' % size,
    number=repeat), " # numpy min/max"

print timeit.timeit(
    'untitled.minmax1(a)',
    setup='import numpy as np; import untitled; a = np.arange(%d, dtype=np.float32)' % size,
    number=repeat), '# minmax1'

print timeit.timeit(
    'untitled.minmax2(a)',
    setup='import numpy as np; import untitled; a = np.arange(%d, dtype=np.float32)' % size,
    number=repeat), '# minmax2'

Результати для мене трохи приголомшливі:

8.61869883537 # numpy min/max
1.60417699814 # minmax1
2.30169081688 # minmax2

Треба сказати, я не цілком її розумію. Якщо порівнювати просто np.minпроти, minmax1і minmax2це все ще програш, тому це не лише питання пам'яті ...

Примітки. Збільшення розміру на коефіцієнт 10**aта зменшення повторення на коефіцієнт 10**a(утримуючи постійний розмір проблеми) змінює продуктивність, але не на перший погляд, послідовно, що свідчить про те, що існує деяка взаємодія між продуктивністю пам'яті та накладними викликами функцій у пітон. Навіть порівнюючи просту minреалізацію у фортран-б'юті нумерів в коефіцієнт приблизно 2 ...


21
Перевага одного проходу - ефективність пам’яті. Особливо, якщо ваш масив достатньо великий, щоб його можна було замінити, це може бути величезним.
Дугал

4
Це не зовсім вірно, його майже вдвічі швидше, тому що для таких типів масивів швидкість пам'яті зазвичай є обмежуючим фактором, тому вона може бути наполовину швидшою ...
seberg

3
Вам не завжди потрібні два чеки. Якщо i < minvalце правда, то i > maxvalце завжди неправда, тому вам потрібно робити в середньому лише 1,5 перевірки за ітерацію, коли друга ifзамінюється на "an" elif.
Фред Фоо

2
Невелика примітка: я сумніваюся, що Cython - це спосіб отримати найбільш оптимізований модуль C, що дзвонить Python. Мета Cython - бути своєрідним Python з анотацією типу, який потім перекладається машиною на C, тоді як f2pyпросто обертає Fortran, кодований вручну, щоб його можна було викликати Python. "Більш справедливий" тест, ймовірно, вручну кодує C, а потім використовує f2py(!), Щоб обернути його для Python. Якщо ви дозволяєте C ++, то Shed Skin може стати найкращим місцем для збалансування легкості кодування та продуктивності.
Джон Y

4
з nummy 1,8 хв і максимізуються на платформах amd64, на моєму core2duo numpy виконує так само, як і цей код fortran. Але одне проходження було б вигідним, якщо масив перевищить розмір великих кеш-процесорів.
jtaylor

23

Існує функція пошуку (max-min) під назвою numpy.ptp, якщо це корисно для вас:

>>> import numpy
>>> x = numpy.array([1,2,3,4,5,6])
>>> x.ptp()
5

але я не думаю, що існує спосіб знайти як мінімум, так і макс. за допомогою одного обходу.

EDIT: ptp просто дзвонить min і max під кришкою


2
Це дратує, тому що, мабуть, шляхом реалізації ptp він повинен відслідковувати максимум та мінімум!
Енді Хайден

1
Або це може просто зателефонувати макс та хв, не впевнений
jterrace

3
@hayden виявляється ptp просто дзвонить max та min
jterrace

1
Це був код масованого масиву; основний код ndarray знаходиться у C. Але виявляється, що код C також повторюється над масивом двічі: github.com/numpy/numpy/blob/… .
Кен Арнольд

20

Ви можете використовувати Numba , який є відомим NumPy динамічним компілятором Python, використовуючи LLVM. Отримана реалізація досить проста і зрозуміла:

import numpy
import numba


@numba.jit
def minmax(x):
    maximum = x[0]
    minimum = x[0]
    for i in x[1:]:
        if i > maximum:
            maximum = i
        elif i < minimum:
            minimum = i
    return (minimum, maximum)


numpy.random.seed(1)
x = numpy.random.rand(1000000)
print(minmax(x) == (x.min(), x.max()))

Це також повинно бути швидшим, ніж реалізація Numpy min() & max(). І все без необхідності писати єдиний рядок коду C / Fortran.

Зробіть власні тести на працездатність, оскільки це завжди залежить від вашої архітектури, ваших даних, версій вашого пакета ...


2
> Це також повинно бути швидшим, ніж реалізація min () & max () Numpy. Я не думаю, що це правильно. numpy не є рідним python - це C. `` x = numpy.random.rand (10000000) t = час () для i в діапазоні (1000): minmax (x) print ('numba', time () - t) t = час () для i в діапазоні (1000): x.min () x.max () print ('numpy', time () - t) `` Результати в: ('numba', 10.299750089645386 ) ('numpy', 9.898081064224243)
Authman Apatira

1
@AuthmanApatira: Так, орієнтири завжди такі, тому я сказав, що це " слід " (бути швидшим) і " робити свої власні тести на ефективність, оскільки це завжди залежить від вашої архітектури, ваших даних ... ". У моєму випадку я спробував із 3-ма комп’ютерами і отримав такий самий результат (Numba був швидшим, ніж Numpy), але у вашому комп'ютері результати можуть відрізнятися ... Чи ви намагалися виконати numbaфункцію один раз перед еталоном, щоб переконатися, що він компільований JIT ?. Крім того, якщо ви використовуєте ipythonдля простоти, я б запропонував вам використовувати %timeit whatever_code()для вимірювання часу виконання.
Пеке

3
@AuthmanApatira: У будь-якому випадку, що я намагався показати з цією відповіддю, це те, що іноді код Python (в даному випадку JIT-компільований з Numba) може бути таким же швидким, як і найшвидша бібліотека, складена на C (принаймні, ми говоримо про той самий порядок величини), що вражає, враховуючи, що ми писали не що інше, як чистий код Python, чи не згодні ви? ^^
Пеке

Я згоден =) Також дякую за поради в попередньому коментарі щодо юпітера та компіляції функції один раз поза тимчасовим кодом.
Автман Апатіра

1
Просто наткнувшись на це, не те, що це має значення в практичних випадках, але elifдозволяє ваш мінімум бути більшим, ніж ваш макс. Наприклад, з масивом довжиною 1 макс буде будь-яким, що це значення, а min - + нескінченність. Не велика справа для одноразового, але не гарного коду, щоб кинути глибоко в живіт виробничого звіра.
Майк Вільямсон

12

Загалом, ви можете зменшити кількість порівнянь алгоритму minmax, обробляючи два елементи за один раз і лише порівнюючи менший до тимчасового мінімуму, а більший - до тимчасового максимуму. В середньому потрібно лише 3/4 порівнянь, ніж наївний підхід.

Це може бути реалізовано в c або fortran (або будь-якій іншій мові низького рівня) і має бути майже неперевершеною з точки зору продуктивності. Я використовую щоб проілюструвати принцип і отримати дуже швидку, незалежну від стилю реалізацію:

import numba as nb
import numpy as np

@nb.njit
def minmax(array):
    # Ravel the array and return early if it's empty
    array = array.ravel()
    length = array.size
    if not length:
        return

    # We want to process two elements at once so we need
    # an even sized array, but we preprocess the first and
    # start with the second element, so we want it "odd"
    odd = length % 2
    if not odd:
        length -= 1

    # Initialize min and max with the first item
    minimum = maximum = array[0]

    i = 1
    while i < length:
        # Get the next two items and swap them if necessary
        x = array[i]
        y = array[i+1]
        if x > y:
            x, y = y, x
        # Compare the min with the smaller one and the max
        # with the bigger one
        minimum = min(x, minimum)
        maximum = max(y, maximum)
        i += 2

    # If we had an even sized array we need to compare the
    # one remaining item too.
    if not odd:
        x = array[length]
        minimum = min(x, minimum)
        maximum = max(x, maximum)

    return minimum, maximum

Це, безумовно, швидше, ніж наївний підхід, який представив Пеке :

arr = np.random.random(3000000)
assert minmax(arr) == minmax_peque(arr)  # warmup and making sure they are identical 
%timeit minmax(arr)            # 100 loops, best of 3: 2.1 ms per loop
%timeit minmax_peque(arr)      # 100 loops, best of 3: 2.75 ms per loop

Як і очікувалося, нова реалізація minmax займає лише приблизно 3/4 часу, який займає наївне виконання ( 2.1 / 2.75 = 0.7636363636363637)


1
На моїй машині ваше рішення не швидше, ніж у Пеке. Нумба 0,33.
Джон Цвінк

@johnzwinck Ви запустили тест у моїй відповіді на інший? Якщо так, ви могли б цим поділитися? Але це можливо: я помітив деякі регресії і в нових версіях.
MSeifert

Я запустив ваш орієнтир. Часи вашого рішення та @ Peque's були приблизно однакові (~ 2,8 мс).
Джон Цвінк

@JohnZwinck Це дивно, я просто перевірив його ще раз, і на моєму комп’ютері це безумовно швидше. Можливо, це має щось спільне з numba та LLVM, що залежить від обладнання.
MSeifert

Зараз я спробував на іншій машині (бичача робоча станція) і отримав 2,4 мс для вашої проти 2,6 для Peque. Отже, невеликий виграш.
Джон Цвінк

11

Просто, щоб отримати деякі ідеї про числа, які можна було б очікувати, враховуючи такі підходи:

import numpy as np


def extrema_np(arr):
    return np.max(arr), np.min(arr)
import numba as nb


@nb.jit(nopython=True)
def extrema_loop_nb(arr):
    n = arr.size
    max_val = min_val = arr[0]
    for i in range(1, n):
        item = arr[i]
        if item > max_val:
            max_val = item
        elif item < min_val:
            min_val = item
    return max_val, min_val
import numba as nb


@nb.jit(nopython=True)
def extrema_while_nb(arr):
    n = arr.size
    odd = n % 2
    if not odd:
        n -= 1
    max_val = min_val = arr[0]
    i = 1
    while i < n:
        x = arr[i]
        y = arr[i + 1]
        if x > y:
            x, y = y, x
        min_val = min(x, min_val)
        max_val = max(y, max_val)
        i += 2
    if not odd:
        x = arr[n]
        min_val = min(x, min_val)
        max_val = max(x, max_val)
    return max_val, min_val
%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True


import numpy as np


cdef void _extrema_loop_cy(
        long[:] arr,
        size_t n,
        long[:] result):
    cdef size_t i
    cdef long item, max_val, min_val
    max_val = arr[0]
    min_val = arr[0]
    for i in range(1, n):
        item = arr[i]
        if item > max_val:
            max_val = item
        elif item < min_val:
            min_val = item
    result[0] = max_val
    result[1] = min_val


def extrema_loop_cy(arr):
    result = np.zeros(2, dtype=arr.dtype)
    _extrema_loop_cy(arr, arr.size, result)
    return result[0], result[1]
%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True


import numpy as np


cdef void _extrema_while_cy(
        long[:] arr,
        size_t n,
        long[:] result):
    cdef size_t i, odd
    cdef long x, y, max_val, min_val
    max_val = arr[0]
    min_val = arr[0]
    odd = n % 2
    if not odd:
        n -= 1
    max_val = min_val = arr[0]
    i = 1
    while i < n:
        x = arr[i]
        y = arr[i + 1]
        if x > y:
            x, y = y, x
        min_val = min(x, min_val)
        max_val = max(y, max_val)
        i += 2
    if not odd:
        x = arr[n]
        min_val = min(x, min_val)
        max_val = max(x, max_val)
    result[0] = max_val
    result[1] = min_val


def extrema_while_cy(arr):
    result = np.zeros(2, dtype=arr.dtype)
    _extrema_while_cy(arr, arr.size, result)
    return result[0], result[1]

( extrema_loop_*()підходи схожі на запропоновані тут , тоді як extrema_while_*()підходи базуються на коді звідси )

Наступні терміни:

bm

вказують на те extrema_while_*(), що найшвидші та extrema_while_nb()найшвидші. У будь-якому випадку також рішення extrema_loop_nb()та extrema_loop_cy()рішення перевершують підхід, призначений лише для NumPy (використовуючи np.max()та np.min()окремо).

Нарешті, зауважте, що жодне з них не є таким гнучким, як np.min()/ np.max()(з точки зору підтримки n-dim, axisпараметра тощо).

(повний код доступний тут )


2
Здається, ви можете отримати додаткову 10% швидкість, якщо використовувати @njit (fastmath = True)extrema_while_nb
argenisleon

10

Ніхто не згадував numpy.percentile , тому я думав, що буду. Якщо ви запитаєте про [0, 100]процентилі, він дасть вам масив з двох елементів, min (0-й перцентиль) і max (100-й перцентиль).

Однак це не відповідає цілі ОП: це не швидше, ніж мінімум та максимум окремо. Це, мабуть, пов’язано з деякою технікою, яка дозволила б отримати неекстремальні процентилі (складніша проблема, яка повинна тривати більше часу).

In [1]: import numpy

In [2]: a = numpy.random.normal(0, 1, 1000000)

In [3]: %%timeit
   ...: lo, hi = numpy.amin(a), numpy.amax(a)
   ...: 
100 loops, best of 3: 4.08 ms per loop

In [4]: %%timeit
   ...: lo, hi = numpy.percentile(a, [0, 100])
   ...: 
100 loops, best of 3: 17.2 ms per loop

In [5]: numpy.__version__
Out[5]: '1.14.4'

Майбутню версію Numpy можна поставити в особливий випадок, щоб пропустити звичайний відсотковий розрахунок, якщо тільки [0, 100]вони вимагаються. Не додаючи нічого в інтерфейс, є спосіб попросити Numpy за min та max за один дзвінок (всупереч сказаному в прийнятій відповіді), але стандартна реалізація бібліотеки не використовує цей випадок, щоб зробити це вартий.


9

Це стара нитка, але все одно, якщо хтось ще раз на це перегляне ...

Шукаючи одночасно min і max, можна зменшити кількість порівнянь. Якщо ви порівнюєте поплавці (що, мабуть, це є), це може заощадити певний час, хоча і не складність обчислень.

Замість (код Python):

_max = ar[0]
_min=  ar[0]
for ii in xrange(len(ar)):
    if _max > ar[ii]: _max = ar[ii]
    if _min < ar[ii]: _min = ar[ii]

ви можете спочатку порівняти два суміжних значення в масиві, а потім порівняти лише менший з поточним мінімумом, а більший із поточним максимумом:

## for an even-sized array
_max = ar[0]
_min = ar[0]
for ii in xrange(0, len(ar), 2)):  ## iterate over every other value in the array
    f1 = ar[ii]
    f2 = ar[ii+1]
    if (f1 < f2):
        if f1 < _min: _min = f1
        if f2 > _max: _max = f2
    else:
        if f2 < _min: _min = f2
        if f1 > _max: _max = f1

Код тут написаний на Python, чітко для швидкості ви використовуєте C або Fortran або Cython, але таким чином ви робите 3 порівняння за ітерацію, з ітераціями len (ar) / 2, даючи порівняння 3/2 * len (ar). На відміну від цього, роблячи порівняння "очевидним способом", ви робите два порівняння за ітерацію, що призводить до 2 * len (ar) порівнянь. Економить 25% часу порівняння.

Можливо, хтось одного дня знайде це корисним.


6
Ви це орієнтували? на сучасному апаратному забезпеченні x86 у вас є машинні інструкції для min та max, як це було використано у першому варіанті, вони уникають потреби в гілках, тоді як ваш код встановлює залежність від керування, яка, ймовірно, не відображається також на апаратному забезпеченні.
jtaylor

Насправді я не був. Зробиться, якщо я отримаю шанс. Я думаю, що цілком зрозуміло, що чистий код python втратить руки до будь-якої розумної компільованої реалізації, але мені цікаво, чи можна побачити прискорення в Cython ...
Беннет

13
Тут є реалізація minmax в numpy, під кришкою, яку використовує np.bincount, дивіться тут . Він не використовує хитрість, яку ви вказуєте, бо виявився в 2 рази повільніше, ніж наївний підхід. Існує посилання від PR до деяких вичерпних орієнтирів обох методів.
Хайме

5

З першого погляду, здається, робиться трюк:numpy.histogram

count, (amin, amax) = numpy.histogram(a, bins=1)

... але якщо ви подивитесь на джерело цієї функції, воно просто дзвонить a.min()і a.max()незалежно, і тому не вдається уникнути проблем щодо продуктивності, розглянутих у цьому питанні. :-(

Так само scipy.ndimage.measurements.extremaвиглядає можливість, але вона теж просто дзвонить a.min()і a.max()незалежно.


3
np.histogramне завжди для цього працює, оскільки повернені (amin, amax)значення є мінімальними та максимальними значеннями бін. Якщо я, наприклад a = np.zeros(10), np.histogram(a, bins=1)повертається (array([10]), array([-0.5, 0.5])). Користувач шукає (amin, amax)= (0, 0) у такому випадку.
eclark

3

Мені все-таки варто було докласти зусиль, тому я запропоную тут найскладніше і найменш елегантне рішення для тих, хто може зацікавити. Моє рішення - реалізувати багатопотоковий min-max в алгоритмі одного проходу в C ++ і використовувати це для створення модуля розширення Python. Цей зусилля вимагає трохи накладних витрат, щоб навчитися використовувати API Python та NumPy C / C ++, і тут я покажу код і дам кілька невеликих пояснень та посилань для тих, хто бажає піти цим шляхом.

Мін / Макс

Тут немає нічого надто цікавого. Масив розбитий на шматки розміру length / workers. Min / max обчислюється для кожного фрагмента в a future, який потім сканується на глобальний min / max.

    // mt_np.cc
    //
    // multi-threaded min/max algorithm

    #include <algorithm>
    #include <future>
    #include <vector>

    namespace mt_np {

    /*
     * Get {min,max} in interval [begin,end)
     */
    template <typename T> std::pair<T, T> min_max(T *begin, T *end) {
      T min{*begin};
      T max{*begin};
      while (++begin < end) {
        if (*begin < min) {
          min = *begin;
          continue;
        } else if (*begin > max) {
          max = *begin;
        }
      }
      return {min, max};
    }

    /*
     * get {min,max} in interval [begin,end) using #workers for concurrency
     */
    template <typename T>
    std::pair<T, T> min_max_mt(T *begin, T *end, int workers) {
      const long int chunk_size = std::max((end - begin) / workers, 1l);
      std::vector<std::future<std::pair<T, T>>> min_maxes;
      // fire up the workers
      while (begin < end) {
        T *next = std::min(end, begin + chunk_size);
        min_maxes.push_back(std::async(min_max<T>, begin, next));
        begin = next;
      }
      // retrieve the results
      auto min_max_it = min_maxes.begin();
      auto v{min_max_it->get()};
      T min{v.first};
      T max{v.second};
      while (++min_max_it != min_maxes.end()) {
        v = min_max_it->get();
        min = std::min(min, v.first);
        max = std::max(max, v.second);
      }
      return {min, max};
    }
    }; // namespace mt_np

Модуль розширення Python

Ось де все починає бути негарним ... Один із способів використання коду C ++ у Python - це реалізація модуля розширення. Цей модуль можна побудувати та встановити за допомогою distutils.coreстандартного модуля. Повний опис того, що це спричиняє, висвітлено в документації на Python: https://docs.python.org/3/extending/extending.html . ПРИМІТКА. Звичайно, існують інші способи отримання подібних результатів, щоб процитувати https://docs.python.org/3/extending/index.html#extending-index :

Цей посібник охоплює лише основні інструменти для створення розширень, що надаються як частина цієї версії CPython. Сторонні інструменти, такі як Cython, cffi, SWIG та Numba, пропонують як простіші, так і складніші підходи до створення розширень C і C ++ для Python.

По суті, цей маршрут, мабуть, більше академічний, ніж практичний. Маючи на увазі, що я зробив далі, було, приклеївшись близько до підручника, створити файл модуля. Це по суті котло для дистрибутивів, щоб знати, що робити з вашим кодом і створити з нього модуль Python. Перш ніж робити щось із цього, можливо, розумно створити віртуальне середовище Python, щоб ви не забруднили ваші системні пакети (див. Https://docs.python.org/3/library/venv.html#module-venv ).

Ось файл модуля:

// mt_np_forpy.cc
//
// C++ module implementation for multi-threaded min/max for np

#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION

#include <python3.6/numpy/arrayobject.h>

#include "mt_np.h"

#include <cstdint>
#include <iostream>

using namespace std;

/*
 * check:
 *  shape
 *  stride
 *  data_type
 *  byteorder
 *  alignment
 */
static bool check_array(PyArrayObject *arr) {
  if (PyArray_NDIM(arr) != 1) {
    PyErr_SetString(PyExc_RuntimeError, "Wrong shape, require (1,n)");
    return false;
  }
  if (PyArray_STRIDES(arr)[0] != 8) {
    PyErr_SetString(PyExc_RuntimeError, "Expected stride of 8");
    return false;
  }
  PyArray_Descr *descr = PyArray_DESCR(arr);
  if (descr->type != NPY_LONGLTR && descr->type != NPY_DOUBLELTR) {
    PyErr_SetString(PyExc_RuntimeError, "Wrong type, require l or d");
    return false;
  }
  if (descr->byteorder != '=') {
    PyErr_SetString(PyExc_RuntimeError, "Expected native byteorder");
    return false;
  }
  if (descr->alignment != 8) {
    cerr << "alignment: " << descr->alignment << endl;
    PyErr_SetString(PyExc_RuntimeError, "Require proper alignement");
    return false;
  }
  return true;
}

template <typename T>
static PyObject *mt_np_minmax_dispatch(PyArrayObject *arr) {
  npy_intp size = PyArray_SHAPE(arr)[0];
  T *begin = (T *)PyArray_DATA(arr);
  auto minmax =
      mt_np::min_max_mt(begin, begin + size, thread::hardware_concurrency());
  return Py_BuildValue("(L,L)", minmax.first, minmax.second);
}

static PyObject *mt_np_minmax(PyObject *self, PyObject *args) {
  PyArrayObject *arr;
  if (!PyArg_ParseTuple(args, "O", &arr))
    return NULL;
  if (!check_array(arr))
    return NULL;
  switch (PyArray_DESCR(arr)->type) {
  case NPY_LONGLTR: {
    return mt_np_minmax_dispatch<int64_t>(arr);
  } break;
  case NPY_DOUBLELTR: {
    return mt_np_minmax_dispatch<double>(arr);
  } break;
  default: {
    PyErr_SetString(PyExc_RuntimeError, "Unknown error");
    return NULL;
  }
  }
}

static PyObject *get_concurrency(PyObject *self, PyObject *args) {
  return Py_BuildValue("I", thread::hardware_concurrency());
}

static PyMethodDef mt_np_Methods[] = {
    {"mt_np_minmax", mt_np_minmax, METH_VARARGS, "multi-threaded np min/max"},
    {"get_concurrency", get_concurrency, METH_VARARGS,
     "retrieve thread::hardware_concurrency()"},
    {NULL, NULL, 0, NULL} /* sentinel */
};

static struct PyModuleDef mt_np_module = {PyModuleDef_HEAD_INIT, "mt_np", NULL,
                                          -1, mt_np_Methods};

PyMODINIT_FUNC PyInit_mt_np() { return PyModule_Create(&mt_np_module); }

У цьому файлі є значне використання як Python, так і API NumPy, для отримання додаткової інформації зверніться: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple та для NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .

Встановлення модуля

Наступне, що потрібно зробити - використовувати distutils для встановлення модуля. Для цього потрібен файл налаштування:

# setup.py

from distutils.core import setup,Extension

module = Extension('mt_np', sources = ['mt_np_module.cc'])

setup (name = 'mt_np', 
       version = '1.0', 
       description = 'multi-threaded min/max for np arrays',
       ext_modules = [module])

Щоб остаточно встановити модуль, виконайте його python3 setup.py installз віртуального середовища.

Тестування модуля

Нарешті, ми можемо перевірити, чи реалізація C ++ насправді перевершує наївне використання NumPy. Для цього ось простий тестовий сценарій:

# timing.py
# compare numpy min/max vs multi-threaded min/max

import numpy as np
import mt_np
import timeit

def normal_min_max(X):
  return (np.min(X),np.max(X))

print(mt_np.get_concurrency())

for ssize in np.logspace(3,8,6):
  size = int(ssize)
  print('********************')
  print('sample size:', size)
  print('********************')
  samples = np.random.normal(0,50,(2,size))
  for sample in samples:
    print('np:', timeit.timeit('normal_min_max(sample)',
                 globals=globals(),number=10))
    print('mt:', timeit.timeit('mt_np.mt_np_minmax(sample)',
                 globals=globals(),number=10))

Ось результати, які я отримав від цього всього:

8  
********************  
sample size: 1000  
********************  
np: 0.00012079699808964506  
mt: 0.002468645994667895  
np: 0.00011947099847020581  
mt: 0.0020772050047526136  
********************  
sample size: 10000  
********************  
np: 0.00024697799381101504  
mt: 0.002037393998762127  
np: 0.0002713389985729009  
mt: 0.0020942929986631498  
********************  
sample size: 100000  
********************  
np: 0.0007130410012905486  
mt: 0.0019842900001094677  
np: 0.0007540129954577424  
mt: 0.0029724110063398257  
********************  
sample size: 1000000  
********************  
np: 0.0094779249993735  
mt: 0.007134920000680722  
np: 0.009129883001151029  
mt: 0.012836456997320056  
********************  
sample size: 10000000  
********************  
np: 0.09471094200125663  
mt: 0.0453535050037317  
np: 0.09436299200024223  
mt: 0.04188535599678289  
********************  
sample size: 100000000  
********************  
np: 0.9537652180006262  
mt: 0.3957935369980987  
np: 0.9624398809974082  
mt: 0.4019058070043684  

Вони набагато менш обнадійливі, ніж результати, зазначені раніше в потоці, який вказував десь приблизно на 3,5-кратне прискорення, і не включав багатопотоковість. Результати, які я досяг, є дещо розумними, я б очікував, що накладні витрати на нитки та будуть домінувати в часі, поки масиви не стануть дуже великими, і в цей момент збільшення продуктивності почне наближатися до std::thread::hardware_concurrencyx збільшення.

Висновок

Звичайно, є можливість для оптимізації додатків до певного коду NumPy, зокрема, що стосується багатопотокової передачі. Варто чи ні варто докладати зусиль, мені не зрозуміло, але це, звичайно, здається гарною вправою (чи що-небудь). Я думаю, що, можливо, вивчення деяких таких "сторонніх інструментів", як Cython, може бути кращим використанням часу, але хто знає.


1
Я починаю вивчати ваш код, знаю деякі C ++, але все ще не використовую std :: future та std :: async. У вашій функції шаблону 'min_max_mt', як знає, що кожен працівник закінчив між звільненням та отриманням результатів? (Просять просто зрозуміти, не кажучи, що в цьому нічого поганого)
ChrCury78,

Лінія v = min_max_it->get();. У getметоді блокується , поки результат не буде готовий і повертає його. Оскільки цикл проходить через кожне майбутнє, він не закінчиться, поки вони не будуть виконані. future.get ()
Натан Чаппелл

0

Найкоротший спосіб, який я придумав, це:

mn, mx = np.sort(ar)[[0, -1]]

Але оскільки він сортує масив, він не найефективніший.

Ще один короткий шлях:

mn, mx = np.percentile(ar, [0, 100])

Це повинно бути більш ефективним, але результат розраховується, і повертається поплавок.


Соромно, що ці два є найповільнішими рішеннями порівняно з іншими на цій сторінці: m = np.min (a); M = np.max (a) -> 0,54002 ||| m, M = f90_minmax1 (a) -> 0.72134 ||| m, M = numba_minmax (a) -> 0,77323 ||| m, M = np.sort (a) [[0, -1]] -> 12.01456 ||| m, M = np.percentile (a, [0, 100]) -> 11.09418 ||| за секунди за 10000 повторень для масиву елементів 100k
Ісаїй
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.