6.19. Leistung

Dieselben Designentscheidungen, die numpy auf der Kamera schnell machen – Bibliotheksaufrufe über ganze Arrays, gepackte typisierte Puffer, Views, die sich Daten mit ihrer Quelle teilen – bringen auch eine Reihe von Gewohnheiten mit sich, die man kennen sollte. Die Seite Form und Strides hat bereits die Layout-Regel der letzten Achse behandelt; diese Seite katalogisiert die Allokations- und dtype-Gewohnheiten, die in einer Streaming-Schleife am wichtigsten sind.

6.19.1. Wählen Sie einen sinnvollen dtype

Der Standard-dtype jedes Konstruktors ist float. Übergeben Sie für Daten, die von Natur aus 8-Bit oder 16-Bit sind – ADC-Abtastwerte, Bildpixel, Sensormesswerte – dtype= explizit an einen der Integer-Typen:

adc = np.array(adc_samples, dtype=np.uint16)

Die RAM-Einsparung beträgt 2x für uint16 und 4x für uint8 gegenüber dem 4-Byte-Standard float. Auch die Berechnungen laufen schneller, weil die Integer-Codepfade innerhalb von numpy schlanker sind als die generischen Float-Pfade. Die auf Dtypes behandelte Integer-Überlaufregel gilt – casten Sie vor Arithmetik, die überlaufen könnte, in einen breiteren Typ.

6.19.2. Bevorzugen Sie ein ndarray gegenüber einem Iterable

Die meisten Reduktionen und universellen Funktionen akzeptieren entweder ein Iterable oder ein ndarray:

np.sum([1, 2, 3, 4, 5])               # works, but slow
np.sum(np.array([1, 2, 3, 4, 5]))     # ~3x faster

Die Iterable-Form zwingt numpy dazu, die Eingabe ein Python-Objekt nach dem anderen durchzugehen und jedes in eine Zahl umzuwandeln, bevor es verwendet werden kann. Bei einem ndarray ist die Umwandlung bereits erfolgt, und der Aufruf läuft direkt durch den gepackten Puffer.

Wenn dieselben Daten mehr als einmal verwendet werden, erstellen Sie das ndarray einmal und reichen Sie es herum. Wenn die Daten nur als Python-Liste existieren und einmalig konsumiert werden, können die Umwandlungskosten den Geschwindigkeitsvorteil aufwiegen – der array()-Konstruktor selbst muss die Liste durchlaufen und Speicher allozieren.

6.19.3. Bevorzugen Sie Views gegenüber Kopien

Slicing, Indizierung entlang einer einzelnen Achse eines höherrangigen Arrays, reshape(), transpose() und frombuffer() geben allesamt Views zurück, die sich Daten mit der Quelle teilen. Sie sind im Wesentlichen kostenlos.

copy(), flatten(), boolesche Indizierung (a[mask]) und jeder arithmetische Ausdruck allozieren eine Kopie. Greifen Sie nur dann darauf zurück, wenn wirklich ein unabhängiger Puffer benötigt wird.

Im Zweifelsfall gibt ndinfo() die Position des zugrunde liegenden Puffers aus; zwei Arrays, die dieselbe Adresse melden, teilen sich ihre Daten. Die vollständige View-gegen-Kopie-Tabelle finden Sie auf Views und Kopien.

6.19.4. Einmal allozieren, dann schreiben

Die größte einzelne Leistungsfalle auf der Kamera ist das Allozieren frischer Arrays innerhalb einer Schleife, die viele Male pro Sekunde läuft. Jedes neue ndarray fordert von der Kamera RAM an, und häufige frische Allokationen verschwenden ihn.

Die meisten universellen Funktionen akzeptieren out=, sodass das Ergebnis in ein bereits existierendes Array geschrieben werden kann:

x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512)        # allocate once

while True:
    np.sin(x, out=y)
    # use y ...

image.Image.to_ndarray() akzeptiert aus demselben Grund buffer=; spectrogram() und die Konverter im Stil von from_int32_buffer() akzeptieren sowohl out= als auch scratchpad=. Allozieren Sie alles einmal und verwenden Sie es wieder.

6.19.5. Verwenden Sie In-Place-Operatoren

b = b + 1 alloziert ein Temporäres in der Größe von b, kopiert und weist neu zu. b += 1 modifiziert b direkt:

# makes a temporary
b = b + 1

# no temporary
b += 1

Dieselbe Idee gilt für zusammengesetzte Ausdrücke. a + b * c alloziert ein Temporäres für b * c. Das Aufteilen des Ausdrucks in einfache Teilzuweisungen, die in einen vorab allozierten Puffer schreiben, eliminiert die Temporären:

# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2

# zero temporaries
out[:]  = a
out    += b
out    *= 2

6.19.6. Bauen Sie das Ergebnis auf, statt daran anzuhängen

ndarray hat kein append – absichtlich. Ein Array zu vergrößern würde bedeuten, einen frischen, größeren Puffer zu allozieren und den alten Inhalt hineinzukopieren. Allozieren Sie auf einem Mikrocontroller die endgültige Größe vorab und füllen Sie sie:

out = np.zeros(N, dtype=np.float)
for i in range(N):
    out[i] = some_calculation(i)

Wenn N wirklich nicht im Voraus bekannt ist, schreiben Sie in eine Python-list und konvertieren Sie am Ende einmalig mit array().

6.19.7. Slice-Zuweisung statt neuer Arrays

Viele Muster nach dem Prinzip „baue ein neues Array aus Teilen anderer“ lassen sich als Slice-Zuweisungen in einen vorab allozierten Puffer ausdrücken, statt bei jedem Aufruf neu zu allozieren.

Ein gleitendes Fenster über einen Strom von Abtastwerten – die Grundlage eines gleitenden Mittelwertfilters – ist der klassische Fall. Der Puffer enthält die letzten N Abtastwerte; bei jeder Iteration wird der älteste verworfen und der neueste angehängt. Die naheliegende Form baut den Puffer bei jeder Iteration neu auf:

while True:
    sample = read_sample()
    buf = np.concatenate((buf[1:],              # new buffer every loop
                          np.array([sample])))
    avg = np.mean(buf)

Das ist eine frische Allokation – und eine Kopie von N - 1 Elementen – pro Abtastwert. Die Slice-Zuweisungsform verschiebt an Ort und Stelle:

N   = 16
buf = np.zeros(N, dtype=np.float)               # allocate once

while True:
    sample   = read_sample()
    buf[:-1] = buf[1:]                          # shift left by one
    buf[-1]  = sample                           # append at the end
    avg      = np.mean(buf)

buf[:-1] = buf[1:] ist die interessante Zeile: zwei überlappende Views in denselben Puffer, wobei das rechtsseitige Slice von einem Ende gelesen und an das andere geschrieben wird. numpy durchläuft den zugrunde liegenden Speicher in der Reihenfolge, die die In-Place-Verschiebung sicher macht. Innerhalb der Schleife wird nie ein neues Array alloziert.

6.19.8. Achten Sie auf boolesche Masken in Streaming-Schleifen

Boolesche Indizierung und where() erzeugen bei jedem Aufruf ein neues Array – die Größe des Ergebnisses hängt von den Daten ab, sodass kein vorab allozierter Puffer die Allokation auffangen kann. Wiederholtes Erstellen von Masken in einer Streaming-Schleife füllt den RAM mit Wegwerf-Arrays. Ein periodisches gc.collect() gibt den Speicher zurück:

import gc

for i in range(1000):
    mask = a < threshold
    _    = a[mask]
    if i % 100 == 0:
        gc.collect()

Derselbe Vorbehalt gilt für zusammengesetzte boolesche Ausdrücke wie (a > lo) & (a < hi) – jeder Operator alloziert ein neues bool-Array. Wenn eine Maske wiederverwendet wird, erstellen Sie sie einmal und behalten Sie sie:

mask = a < threshold
foo[mask] = 0
bar[mask] = 1