Was ist die empfohlene Methode zum Zuweisen von Speicher für eine typisierte Speicheransicht?

70

In der Cython-Dokumentation zu typisierten Speicheransichten sind drei Möglichkeiten zum Zuweisen zu einer typisierten Speicheransicht aufgeführt:

  1. von einem rohen C-Zeiger,
  2. von einem np.ndarrayund
  3. von a cython.view.array.

Angenommen, ich habe keine Daten von außen an meine Cython-Funktion übergeben, sondern möchte stattdessen Speicher zuweisen und als zurückgeben. Welche np.ndarraydieser Optionen habe ich ausgewählt? Nehmen Sie außerdem an, dass die Größe dieses Puffers keine Kompilierungszeitkonstante ist, dh ich kann sie nicht auf dem Stapel zuordnen, sondern mallocfür Option 1.

Die 3 Optionen würden daher ungefähr so ​​aussehen:

from libc.stdlib cimport malloc, free
cimport numpy as np
from cython cimport view

np.import_array()

def memview_malloc(int N):
    cdef int * m = <int *>malloc(N * sizeof(int))
    cdef int[::1] b = <int[:N]>m
    free(<void *>m)

def memview_ndarray(int N):
    cdef int[::1] b = np.empty(N, dtype=np.int32)

def memview_cyarray(int N):
    cdef int[::1] b = view.array(shape=(N,), itemsize=sizeof(int), format="i")

Was mich überrascht ist, dass Cython in allen drei Fällen ziemlich viel Code für die Speicherzuweisung generiert , insbesondere für einen Aufruf von __Pyx_PyObject_to_MemoryviewSlice_dc_int. Dies deutet darauf hin (und ich könnte mich hier irren, meine Einsicht in das Innenleben von Cython ist sehr begrenzt), dass zuerst ein Python-Objekt erstellt und dann in eine Speicheransicht "umgewandelt" wird, was als unnötiger Aufwand erscheint.

Ein einfacher Benchmark zeigt keinen großen Unterschied zwischen den drei Methoden, wobei 2. mit geringem Abstand der schnellste ist.

Welche der drei Methoden wird empfohlen? Oder gibt es eine andere, bessere Option?

Folgefrage: Ich möchte das Ergebnis endlich als zurückgeben np.ndarray, nachdem ich mit dieser Speicheransicht in der Funktion gearbeitet habe. Ist eine typisierte Speicheransicht die beste Wahl, oder würde ich lieber einfach die alte Pufferschnittstelle wie unten verwenden, um eine zu erstellen ndarray?

cdef np.ndarray[DTYPE_t, ndim=1] b = np.empty(N, dtype=np.int32)
kynan
quelle
2
Ausgezeichnete Frage, ich frage mich über etwas Ähnliches.
AlexE
Ihr Benchmark ist die beste Antwort, die mir bekannt ist. Um die folgende Frage zu beantworten, können Sie Ihr NumPy-Array einfach auf die übliche Weise deklarieren (Sie müssen nicht einmal die alte Typschnittstelle verwenden) und dann so etwas wie cdef int[:] arrview = arreine Ansicht des gleichen Speichers erhalten, der für das NumPy-Array verwendet wird . Sie können die Ansicht für die schnelle Indizierung und zum Übergeben von Slices zwischen Cython-Funktionen verwenden, während Sie weiterhin über das NumPy-Array auf die NumPy-Funktionen zugreifen können. Wenn Sie fertig sind, können Sie einfach das NumPy-Array zurückgeben.
IanH
Es gibt hier eine gute verwandte Frage ... wo Sie sehen können, dass np.empty langsam sein kann ...
Saullo GP Castro

Antworten:

77

Suchen Sie hier nach einer Antwort.

Die Grundidee ist, dass Sie wollen cpython.array.arrayund cpython.array.clone( nicht cython.array.* ):

from cpython.array cimport array, clone

# This type is what you want and can be cast to things of
# the "double[:]" syntax, so no problems there
cdef array[double] armv, templatemv

templatemv = array('d')

# This is fast
armv = clone(templatemv, L, False)

BEARBEITEN

Es stellt sich heraus, dass die Benchmarks in diesem Thread Müll waren. Hier ist mein Set mit meinen Timings:

# cython: language_level=3
# cython: boundscheck=False
# cython: wraparound=False

import time
import sys

from cpython.array cimport array, clone
from cython.view cimport array as cvarray
from libc.stdlib cimport malloc, free
import numpy as numpy
cimport numpy as numpy

cdef int loops

def timefunc(name):
    def timedecorator(f):
        cdef int L, i

        print("Running", name)
        for L in [1, 10, 100, 1000, 10000, 100000, 1000000]:
            start = time.clock()
            f(L)
            end = time.clock()
            print(format((end-start) / loops * 1e6, "2f"), end=" ")
            sys.stdout.flush()

        print("μs")
    return timedecorator

print()
print("INITIALISATIONS")
loops = 100000

@timefunc("cpython.array buffer")
def _(int L):
    cdef int i
    cdef array[double] arr, template = array('d')

    for i in range(loops):
        arr = clone(template, L, False)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("cpython.array memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr
    cdef array template = array('d')

    for i in range(loops):
        arr = clone(template, L, False)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("cpython.array raw C type")
def _(int L):
    cdef int i
    cdef array arr, template = array('d')

    for i in range(loops):
        arr = clone(template, L, False)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("numpy.empty_like memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr
    template = numpy.empty((L,), dtype='double')

    for i in range(loops):
        arr = numpy.empty_like(template)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("malloc")
def _(int L):
    cdef int i
    cdef double* arrptr

    for i in range(loops):
        arrptr = <double*> malloc(sizeof(double) * L)
        free(arrptr)

    # Prevents dead code elimination
    str(arrptr[0])

@timefunc("malloc memoryview")
def _(int L):
    cdef int i
    cdef double* arrptr
    cdef double[::1] arr

    for i in range(loops):
        arrptr = <double*> malloc(sizeof(double) * L)
        arr = <double[:L]>arrptr
        free(arrptr)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("cvarray memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr

    for i in range(loops):
        arr = cvarray((L,),sizeof(double),'d')

    # Prevents dead code elimination
    str(arr[0])



print()
print("ITERATING")
loops = 1000

@timefunc("cpython.array buffer")
def _(int L):
    cdef int i
    cdef array[double] arr = clone(array('d'), L, False)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("cpython.array memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr = clone(array('d'), L, False)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("cpython.array raw C type")
def _(int L):
    cdef int i
    cdef array arr = clone(array('d'), L, False)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("numpy.empty_like memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr = numpy.empty((L,), dtype='double')

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("malloc")
def _(int L):
    cdef int i
    cdef double* arrptr = <double*> malloc(sizeof(double) * L)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arrptr[i]

    free(arrptr)

    # Prevents dead-code elimination
    str(d)

@timefunc("malloc memoryview")
def _(int L):
    cdef int i
    cdef double* arrptr = <double*> malloc(sizeof(double) * L)
    cdef double[::1] arr = <double[:L]>arrptr

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    free(arrptr)

    # Prevents dead-code elimination
    str(d)

@timefunc("cvarray memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr = cvarray((L,),sizeof(double),'d')

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

Ausgabe:

INITIALISATIONS
Running cpython.array buffer
0.100040 0.097140 0.133110 0.121820 0.131630 0.108420 0.112160 μs
Running cpython.array memoryview
0.339480 0.333240 0.378790 0.445720 0.449800 0.414280 0.414060 μs
Running cpython.array raw C type
0.048270 0.049250 0.069770 0.074140 0.076300 0.060980 0.060270 μs
Running numpy.empty_like memoryview
1.006200 1.012160 1.128540 1.212350 1.250270 1.235710 1.241050 μs
Running malloc
0.021850 0.022430 0.037240 0.046260 0.039570 0.043690 0.030720 μs
Running malloc memoryview
1.640200 1.648000 1.681310 1.769610 1.755540 1.804950 1.758150 μs
Running cvarray memoryview
1.332330 1.353910 1.358160 1.481150 1.517690 1.485600 1.490790 μs

ITERATING
Running cpython.array buffer
0.010000 0.027000 0.091000 0.669000 6.314000 64.389000 635.171000 μs
Running cpython.array memoryview
0.013000 0.015000 0.058000 0.354000 3.186000 33.062000 338.300000 μs
Running cpython.array raw C type
0.014000 0.146000 0.979000 9.501000 94.160000 916.073000 9287.079000 μs
Running numpy.empty_like memoryview
0.042000 0.020000 0.057000 0.352000 3.193000 34.474000 333.089000 μs
Running malloc
0.002000 0.004000 0.064000 0.367000 3.599000 32.712000 323.858000 μs
Running malloc memoryview
0.019000 0.032000 0.070000 0.356000 3.194000 32.100000 327.929000 μs
Running cvarray memoryview
0.014000 0.026000 0.063000 0.351000 3.209000 32.013000 327.890000 μs

(Der Grund für den Benchmark "Iterationen" ist, dass einige Methoden in dieser Hinsicht überraschend unterschiedliche Eigenschaften aufweisen.)

In der Reihenfolge der Initialisierungsgeschwindigkeit:

malloc: Dies ist eine raue Welt, aber es ist schnell. Wenn Sie viele Dinge zuordnen müssen und eine ungehinderte Iterations- und Indizierungsleistung haben müssen, muss dies der Fall sein. Aber normalerweise bist du eine gute Wette für ...

cpython.array raw C type: Verdammt, es ist schnell. Und es ist sicher. Leider geht es über Python, um auf seine Datenfelder zuzugreifen. Sie können dies vermeiden, indem Sie einen wunderbaren Trick anwenden:

arr.data.as_doubles[i]

das bringt es auf die Standardgeschwindigkeit und entfernt gleichzeitig die Sicherheit! Dies macht dies zu einem wunderbaren Ersatz für malloceine ziemlich referenzgezählte Version!

cpython.array buffer: Mit nur drei- bis viermaliger Einrichtungszeit mallocsieht dies eine wunderbare Wette aus. Leider hat es einen erheblichen Overhead (wenn auch im Vergleich zu den Richtlinien boundscheckund wraparound) gering ). Das heißt, es konkurriert nur wirklich mit Vollsicherheitsvarianten, aber es ist das am schnellsten zu initialisierende. Deine Entscheidung.

cpython.array memoryview: Dies ist jetzt eine Größenordnung langsamer als mallocbeim Initialisieren. Das ist eine Schande, aber es iteriert genauso schnell. Dies ist die Standardlösung, die ich vorschlagen würde, sofern sie nicht boundscheckaktiviert wraparoundist (in diesem Fall cpython.array bufferkönnte dies ein überzeugenderer Kompromiss sein).

Der Rest. Das einzige, was etwas wert ist numpy, ist das, aufgrund der vielen lustigen Methoden, die mit den Objekten verbunden sind. Das war's aber schon.

Veedrac
quelle
Vielen Dank für diese umfassende Umfrage und die Sicherung mit Zahlen!
Kynan
2
Gute Antwort! Habe ich Recht, wenn ich denke, dass nur die reine mallocLösung die Notwendigkeit, die GIL zu erwerben, vollständig umgehen würde? Ich interessiere mich für Möglichkeiten, mehrdimensionale Arrays in parallelen Arbeitsthreads zuzuweisen.
Ali_m
Probieren Sie sie aus und melden Sie sich zurück!
Veedrac
Die Leute scheinen dies nützlich zu finden. Ich werde sehen, ob ich es hinzufügen kann.
Veedrac
1
cpython.array ist bereits unter docs.cython.org/src/tutorial/array.html beschrieben. Der Code sollte so geändert werden, dass er den Trick "arr.data.as_doubles [i]" für den Benchmark "raw C type" enthält, da dies nicht der Fall ist Die Indizierung ist absolut nicht roh (die aktuelle könnte als "Plain Cpython.array" -Indizierung bezeichnet werden, ist aber kein interessanter Datenpunkt).
Andreas
9

Als Folge der Antwort von Veedrac: Beachten Sie, dass die memoryviewUnterstützung von cpython.arrayPython 2.7 derzeit zu Speicherverlusten führt. Dies scheint ein langjähriges Problem zu sein, da es auf der Mailingliste der Cython-Benutzer hier in einem Beitrag vom November 2012 erwähnt wird. Das Ausführen von Veedracs Benchmark-Scrip mit Cython Version 0.22 mit Python 2.7.6 und Python 2.7.9 führt zu a Ein großer Speicherverlust tritt auf, wenn ein cpython.arraymit einer bufferoder einer memoryviewSchnittstelle initialisiert wird. Beim Ausführen des Skripts mit Python 3.4 treten keine Speicherverluste auf. Ich habe einen Fehlerbericht dazu an die Mailingliste der Cython-Entwickler gesendet.

Matt Graham
quelle