NumPy: funzione per max () e min () simultanei


109

numpy.amax () troverà il valore massimo in un array e numpy.amin () farà lo stesso per il valore minimo. Se voglio trovare sia max che min, devo chiamare entrambe le funzioni, il che richiede di passare due volte l'array (molto grande), il che sembra lento.

C'è una funzione nell'API numpy che trova sia max che min con un solo passaggio attraverso i dati?


1
Quanto è grande molto grande? Se ho un po 'di tempo, amaxamin
eseguirò

1
Devo ammettere che "molto grande" è soggettivo. Nel mio caso, sto parlando di array che sono pochi GB.
Stuart Berg

è abbastanza grande. Ho codificato un esempio per calcolarlo in fortran (anche se non conosci fortran, dovrebbe essere abbastanza facile capire il codice). Fa davvero la differenza eseguirlo da fortran rispetto a quello di numpy. (Presumibilmente, dovresti essere in grado di ottenere le stesse prestazioni da C ...) Non sono sicuro - Suppongo che avremmo bisogno di uno sviluppatore numpy per commentare il motivo per cui le mie funzioni funzionano molto meglio delle loro ...
mgilson

Naturalmente, questa non è certo un'idea nuova. Ad esempio, la libreria boost minmax (C ++) fornisce un'implementazione dell'algoritmo che sto cercando.
Stuart Berg

3
Non proprio una risposta alla domanda posta, ma probabilmente di interesse per le persone su questo thread. Ha chiesto a NumPy di ​​aggiungere minmaxalla libreria in questione ( github.com/numpy/numpy/issues/9836 ).
Jakirkham

Risposte:


49

C'è una funzione nell'API numpy che trova sia max che min con un solo passaggio attraverso i dati?

No. Al momento della stesura di questo articolo, non esiste tale funzione. (E sì, se ci fossero una tale funzione, le sue prestazioni sarebbe significativamente meglio che chiamare numpy.amin()e numpy.amax()successivamente su un array di grandi dimensioni.)


31

Non credo che passare due volte sopra l'array sia un problema. Considera il seguente pseudo-codice:

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

Sebbene qui sia presente solo 1 loop, ci sono ancora 2 controlli. (Invece di avere 2 loop con 1 controllo ciascuno). In realtà l'unica cosa che salvi è l'overhead di 1 loop. Se gli array sono davvero grandi come dici, l'overhead è piccolo rispetto al carico di lavoro del loop effettivo. (Nota che tutto questo è implementato in C, quindi i loop sono comunque più o meno liberi).


EDIT Mi dispiace per voi 4 che avete votato positivamente e avete avuto fiducia in me. Puoi sicuramente ottimizzarlo.

Ecco un po 'di codice fortran che può essere compilato in un modulo python tramite f2py(forse un Cythonguru può venire e confrontarlo con una versione C ottimizzata ...):

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

Compilalo tramite:

f2py -m untitled -c fortran_code.f90

E ora siamo in un posto dove possiamo testarlo:

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'

I risultati sono un po 'sconcertanti per me:

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

Devo dire che non lo capisco completamente. Il confronto solo np.mincon minmax1ed minmax2è ancora una battaglia persa, quindi non è solo un problema di memoria ...

note - L'aumento delle dimensioni di un fattore 10**ae la diminuzione della ripetizione di un fattore 10**a(mantenendo costante la dimensione del problema) cambia le prestazioni, ma non in un modo apparentemente coerente, il che mostra che esiste una certa interazione tra le prestazioni della memoria e il sovraccarico della chiamata di funzione pitone. Anche il confronto di una semplice minimplementazione in fortran batte numpy di un fattore di circa 2 ...


21
Il vantaggio di un singolo passaggio è l'efficienza della memoria. In particolare, se il tuo array è abbastanza grande da essere sostituito, potrebbe essere enorme.
Dougal il

4
Non è del tutto vero, è quasi la metà più veloce, perché con questo tipo di array, la velocità della memoria è solitamente il fattore limitante, quindi può essere la metà più veloce ...
seberg

3
Non hai sempre bisogno di due controlli. Se i < minvalè vero, allora i > maxvalè sempre falso, quindi devi solo eseguire in media 1,5 controlli per iterazione quando il secondo ifviene sostituito da un elif.
Fred Foo

2
Piccola nota: dubito che Cython sia il modo per ottenere il modulo C richiamabile da Python più ottimizzato. L'obiettivo di Cython è quello di essere una sorta di Python con annotazioni di tipo, che viene quindi tradotto automaticamente in C, mentre f2pyesegue solo il wrapping di Fortran codificato a mano in modo che sia richiamabile da Python. Un test "più equo" è probabilmente la codifica manuale del C e quindi l'uso di f2py(!) Per avvolgerlo per Python. Se stai consentendo il C ++, Shed Skin potrebbe essere il punto ideale per bilanciare la facilità di codifica con le prestazioni.
John Y

4
a partire da numpy 1.8 min e max sono vettorizzati su piattaforme amd64, sul mio core2duo numpy funziona come questo codice fortran. Ma un singolo passaggio sarebbe vantaggioso se l'array superasse le dimensioni delle cache della CPU più grandi.
jtaylor

23

C'è una funzione per trovare (max-min) chiamata numpy.ptp se è utile per te:

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

ma non credo che ci sia un modo per trovare sia il minimo che il massimo con una traversata.

EDIT: ptp chiama solo min e max sotto il cofano


2
È fastidioso perché presumibilmente il modo in cui è implementato ptp deve tenere traccia di max e min!
Andy Hayden

1
Oppure potrebbe semplicemente chiamare max e min, non sono sicuro
jterrace

3
@hayden scopre che ptp chiama solo max e min
jterrace

1
Quello era il codice della matrice mascherata; il codice ndarray principale è in C. Ma risulta che anche il codice C itera due volte sull'array: github.com/numpy/numpy/blob/… .
Ken Arnold

20

Potresti usare Numba , che è un compilatore Python dinamico compatibile con NumPy usando LLVM. L'implementazione risultante è piuttosto semplice e chiara:

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

Dovrebbe anche essere più veloce dell'implementazione di Numpy min() & max(). E tutto senza dover scrivere una singola riga di codice C / Fortran.

Fai i tuoi test delle prestazioni, poiché dipende sempre dalla tua architettura, dai tuoi dati, dalle versioni del tuo pacchetto ...


2
> Dovrebbe anche essere più veloce dell'implementazione min () e max () di Numpy. Non credo sia corretto. numpy non è python nativo - è C. `` x = numpy.random.rand (10000000) t = time () for i in range (1000): minmax (x) print ('numba', time () - t) t = time () for i nell'intervallo (1000): x.min () x.max () print ('numpy', time () - t) `` Risultati in: ('numba', 10.299750089645386 ) ('numpy', 9.898081064224243)
Authman Apatira

1
@AuthmanApatira: Sì, i benchmark sono sempre così, ecco perché ho detto che " dovrebbe " (essere più veloce) e " fai i tuoi test delle prestazioni, poiché dipende sempre dalla tua architettura, dai tuoi dati ... ". Nel mio caso, ho provato con 3 computer e ho ottenuto lo stesso risultato (Numba era più veloce di Numpy), ma nel tuo computer i risultati potrebbero differire ... Hai provato a eseguire la numbafunzione una volta prima del benchmark per assicurarti che sia compilato JIT ?. Inoltre, se utilizzi ipython, per semplicità, ti consiglio di utilizzarlo %timeit whatever_code()per misurare il tempo di esecuzione.
Peque

3
@AuthmanApatira: In ogni caso quello che ho cercato di mostrare con questa risposta è che a volte il codice Python (in questo caso compilato JIT con Numba) può essere veloce quanto la libreria compilata in C più veloce (almeno stiamo parlando dello stesso ordine di magnitudine), il che è impressionante considerando che non abbiamo scritto nient'altro che puro codice Python, non sei d'accordo? ^^
Peque

Sono d'accordo =) Inoltre, grazie per i suggerimenti nel commento precedente riguardanti jupyter e la compilazione della funzione una volta al di fuori del codice di temporizzazione.
Authman Apatira

1
Mi sono imbattuto in questo, non che sia importante nei casi pratici, ma elifconsente che il tuo minimo sia maggiore del tuo massimo. Ad esempio, con un array di lunghezza 1, il massimo sarà qualunque sia il valore, mentre il minimo è + infinito. Non è un grosso problema per un codice una tantum, ma non buono da gettare in profondità nella pancia di una bestia da produzione.
Mike Williamson

12

In generale è possibile ridurre la quantità di confronti per un algoritmo minmax elaborando due elementi alla volta e confrontando solo il più piccolo con il minimo temporaneo e quello più grande con il massimo temporaneo. In media sono necessari solo 3/4 dei confronti rispetto a un approccio ingenuo.

Questo potrebbe essere implementato in c o fortran (o qualsiasi altro linguaggio di basso livello) e dovrebbe essere quasi imbattibile in termini di prestazioni. sto usando per illustrare il principio e ottenere un'implementazione molto veloce e indipendente dal tipo:

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

È decisamente più veloce dell'approccio ingenuo presentato da Peque :

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

Come previsto, la nuova implementazione minmax richiede solo circa 3/4 del tempo impiegato dall'implementazione ingenua ( 2.1 / 2.75 = 0.7636363636363637)


1
Sulla mia macchina, la tua soluzione non è più veloce di quella di Peque. Numba 0.33.
John Zwinck

@johnzwinck hai eseguito il benchmark nella mia risposta è diverso? Se è così potresti condividerlo? Ma è possibile: ho notato alcune regressioni anche nelle versioni più recenti.
MSeifert

Ho eseguito il tuo benchmark. I tempi della tua soluzione e di @ Peque erano praticamente gli stessi (~ 2,8 ms).
John Zwinck

@JohnZwinck È strano, l'ho appena provato di nuovo e sul mio computer è decisamente più veloce. Forse ha qualcosa a che fare con numba e LLVM che dipende dall'hardware.
MSeifert

Ho provato su un'altra macchina ora (una workstation robusta) e ho ottenuto 2,4 ms per la tua contro 2,6 per quella di Peque. Quindi, una piccola vittoria.
John Zwinck

11

Giusto per avere qualche idea sui numeri che ci si potrebbe aspettare, visti i seguenti approcci:

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]

(gli extrema_loop_*()approcci sono simili a quanto qui proposto , mentre gli extrema_while_*()approcci sono basati sul codice da qui )

I seguenti tempi:

bm

indicano che extrema_while_*()sono i più veloci, con l' extrema_while_nb()essere più veloci. In ogni caso, anche le soluzioni extrema_loop_nb()e extrema_loop_cy()superano l'approccio solo NumPy (usando np.max()e np.min()separatamente).

Infine, nota che nessuno di questi è flessibile come np.min()/ np.max()(in termini di supporto n-dim, axisparametro, ecc.).

(il codice completo è disponibile qui )


2
Sembra che tu possa ottenere una velocità extra del 10% se usi @njit (fastmath = True)extrema_while_nb
argenisleon

10

Nessuno ha menzionato numpy.percentile , quindi ho pensato di farlo. Se chiedi i [0, 100]percentili, otterrai un array di due elementi, il min (0 ° percentile) e il massimo (100 ° percentile).

Tuttavia, non soddisfa lo scopo dell'OP: non è più veloce di min e max separatamente. Ciò è probabilmente dovuto ad alcuni meccanismi che consentirebbero percentili non estremi (un problema più difficile, che dovrebbe richiedere più tempo).

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'

Una versione futura di Numpy potrebbe inserire un caso speciale per saltare il normale calcolo del percentile se solo [0, 100]richiesto. Senza aggiungere nulla all'interfaccia, c'è un modo per chiedere a Numpy il minimo e il massimo in una chiamata (contrariamente a quanto detto nella risposta accettata), ma l'implementazione standard della libreria non sfrutta questo caso per farlo vale la pena.


9

Questo è un vecchio thread, ma comunque, se qualcuno lo guardasse di nuovo ...

Quando si cercano il minimo e il massimo contemporaneamente, è possibile ridurre il numero di confronti. Se si tratta di float che stai confrontando (cosa che immagino sia) questo potrebbe farti risparmiare tempo, sebbene non complessità computazionale.

Invece di (codice 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]

puoi prima confrontare due valori adiacenti nella matrice, quindi confrontare solo quello più piccolo con il minimo corrente e quello più grande con il massimo corrente:

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

Il codice qui è scritto in Python, chiaramente per la velocità useresti C o Fortran o Cython, ma in questo modo fai 3 confronti per iterazione, con len (ar) / 2 iterazioni, dando 3/2 * len (ar) confronti. Al contrario, facendo il confronto "nel modo più ovvio" esegui due confronti per iterazione, portando a confronti 2 * len (ar). Risparmia il 25% del tempo di confronto.

Forse qualcuno un giorno lo troverà utile.


6
hai valutato questo? sul moderno hardware x86 hai le istruzioni della macchina per min e max come usate nella prima variante, queste evitano la necessità di diramazioni mentre il tuo codice mette in una dipendenza di controllo che probabilmente non si mappa bene con l'hardware.
jtaylor

In realtà non l'ho fatto. Lo farò se ne avrò la possibilità. Penso che sia abbastanza chiaro che il codice Python puro perderà le mani a qualsiasi implementazione compilata sensata, ma mi chiedo se un aumento della velocità potrebbe essere visto in Cython ...
Bennet

13
C'è un'implementazione minmax in numpy, sotto il cofano, usata da np.bincount, vedi qui . Non usa il trucco che hai indicato, perché si è rivelato fino a 2 volte più lento dell'approccio ingenuo. Esiste un collegamento dal PR ad alcuni benchmark completi di entrambi i metodi.
Jaime

5

A prima vista, sembra fare il trucco:numpy.histogram

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

... ma se guardi l' origine per quella funzione, chiama semplicemente a.min()e in modo a.max()indipendente, e quindi non riesce a evitare i problemi di prestazioni affrontati in questa domanda. :-(

Allo stesso modo, scipy.ndimage.measurements.extremasembra una possibilità, ma anch'essa chiama semplicemente a.min()e in modo a.max()indipendente.


3
np.histogramnon sempre funziona per questo perché i (amin, amax)valori restituiti sono per i valori minimo e massimo del bin. Se ho, ad esempio a = np.zeros(10), np.histogram(a, bins=1)restituisce (array([10]), array([-0.5, 0.5])). L'utente sta cercando (amin, amax)= (0, 0) in quel caso.
eclark

3

Ne è valsa comunque la pena per me, quindi proporrò qui la soluzione più difficile e meno elegante per chiunque possa essere interessato. La mia soluzione è implementare un algoritmo min-max multi-thread in un passaggio in C ++ e usarlo per creare un modulo di estensione Python. Questo sforzo richiede un po 'di overhead per imparare a usare le API C / C ++ di Python e NumPy, e qui mostrerò il codice e fornirò alcune piccole spiegazioni e riferimenti per chiunque desideri seguire questo percorso.

Multi-threaded Min / Max

Non c'è niente di troppo interessante qui. L'array è suddiviso in blocchi di dimensioni length / workers. Il min / max viene calcolato per ogni blocco in a future, che viene quindi scansionato per il min / max globale.

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

Il modulo di estensione Python

Qui è dove le cose iniziano a diventare brutte ... Un modo per usare il codice C ++ in Python è implementare un modulo di estensione. Questo modulo può essere costruito e installato utilizzando il distutils.coremodulo standard. Una descrizione completa di ciò che ciò comporta è trattata nella documentazione di Python: https://docs.python.org/3/extending/extending.html . NOTA: ci sono sicuramente altri modi per ottenere risultati simili, per citare https://docs.python.org/3/extending/index.html#extending-index :

Questa guida copre solo gli strumenti di base per la creazione di estensioni fornite come parte di questa versione di CPython. Strumenti di terze parti come Cython, cffi, SWIG e Numba offrono approcci più semplici e sofisticati alla creazione di estensioni C e C ++ per Python.

In sostanza, questo percorso è probabilmente più accademico che pratico. Detto questo, quello che ho fatto dopo è stato, rimanendo abbastanza vicino al tutorial, creare un file di modulo. Questo è essenzialmente boilerplate per distutils per sapere cosa fare con il tuo codice e creare un modulo Python da esso. Prima di fare tutto ciò è probabilmente saggio creare un ambiente virtuale Python in modo da non inquinare i pacchetti di sistema (vedere https://docs.python.org/3/library/venv.html#module-venv ).

Ecco il file del modulo:

// 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); }

In questo file c'è un uso significativo dell'API Python e NumPy, per maggiori informazioni consultare: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple e per NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .

Installazione del modulo

La prossima cosa da fare è utilizzare distutils per installare il modulo. Ciò richiede un file di installazione:

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

Per installare finalmente il modulo, esegui python3 setup.py installdal tuo ambiente virtuale.

Test del modulo

Infine, possiamo verificare se l'implementazione di C ++ supera effettivamente l'uso ingenuo di NumPy. Per farlo, ecco un semplice script di test:

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

Ecco i risultati che ho ottenuto facendo tutto questo:

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  

Questi sono molto meno incoraggianti di quanto i risultati indicano in precedenza nel thread, che indicava un aumento della velocità di circa 3,5 volte e non incorporavano il multi-threading. I risultati che ho ottenuto sono in qualche modo ragionevoli, mi aspetterei che il sovraccarico del threading dominasse il tempo fino a quando gli array non diventassero molto grandi, a quel punto l'aumento delle prestazioni inizierebbe ad avvicinarsi std::thread::hardware_concurrencyall'incremento x.

Conclusione

C'è sicuramente spazio per ottimizzazioni specifiche dell'applicazione per alcuni codici NumPy, sembrerebbe, in particolare per quanto riguarda il multi-threading. Se ne valga la pena o meno non mi è chiaro, ma certamente mi sembra un buon esercizio (o qualcosa del genere). Penso che forse imparare alcuni di quegli "strumenti di terze parti" come Cython possa essere un uso migliore del tempo, ma chi lo sa.


1
Comincio a studiare il tuo codice, conosco un po 'di C ++ ma non ho ancora usato std :: future e std :: async. Nella tua funzione modello 'min_max_mt', come fa a sapere che ogni lavoratore ha terminato tra l'attivazione e il recupero dei risultati? (Chiedendo solo di capire, non dicendo che c'è qualcosa di sbagliato in esso)
ChrCury78,

La linea v = min_max_it->get();. Il getmetodo si blocca finché il risultato non è pronto e lo restituisce. Poiché il ciclo attraversa ogni futuro, non finirà fino a quando non saranno stati completati tutti. future.get ()
Nathan Chappell

0

Il modo più breve che ho trovato è questo:

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

Ma poiché ordina l'array, non è il più efficiente.

Un altro modo breve sarebbe:

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

Questo dovrebbe essere più efficiente, ma il risultato viene calcolato e viene restituito un float.


Vergognosamente, queste due sono le soluzioni più lente rispetto alle altre in questa pagina: 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 ||| in secondi per 10000 ripetizioni per una serie di 100k elementi
Isaías
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.