6.19. Prestaties

Dezelfde ontwerpkeuzes die numpy snel maken op de camera – bibliotheekaanroepen op hele arrays, gecomprimeerde getypeerde buffers, views die data delen met hun bron – leggen ook een aantal gewoonten bloot die het waard zijn om te kennen. De pagina Vorm en strides behandelde al de indelingsregel voor de laatste as; deze pagina catalogiseert de allocatie- en dtype-gewoonten die er het meest toe doen in een streaminglus.

6.19.1. Kies een verstandige dtype

De standaard-dtype van elke constructor is float. Geef voor data die van nature 8-bits of 16-bits is – ADC-samples, afbeeldingspixels, sensormetingen – expliciet dtype= mee met een van de integer-types:

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

De RAM-besparing is 2x voor uint16 en 4x voor uint8 ten opzichte van de standaard float van 4 bytes. De wiskunde loopt ook sneller omdat de integer-codepaden binnen numpy strakker zijn dan de generieke float-paden. De regel voor integer-overflow die behandeld wordt op Dtypes is van toepassing – cast naar een breder type vóór rekenwerk dat kan overflowen.

6.19.2. Geef de voorkeur aan een ndarray boven een iterable

De meeste reducties en universele functies accepteren ofwel een iterable ofwel een ndarray

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

De iterable-vorm dwingt numpy om de invoer één Python-object per keer te doorlopen, waarbij elk object wordt omgezet naar een getal voordat het gebruikt kan worden. Bij een ndarray is de conversie al gedaan en loopt de aanroep rechtstreeks door de gecomprimeerde buffer.

Wanneer dezelfde data meer dan eens wordt gebruikt, bouw de ndarray één keer op en geef deze door. Wanneer de data alleen als Python-lijst bestaat en één keer wordt verbruikt, kan de conversiekost zwaarder wegen dan de snelheidswinst – de constructor array() moet zelf de lijst doorlopen en geheugen alloceren.

6.19.3. Geef de voorkeur aan views boven kopieën

Slicing, indexering op één as van een array van hogere rang, reshape(), transpose() en frombuffer() retourneren allemaal views die data delen met de bron. Ze zijn in wezen gratis.

copy(), flatten(), booleaanse indexering (a[mask]) en elke rekenkundige expressie alloceren een kopie. Grijp er alleen naar wanneer er werkelijk een onafhankelijke buffer nodig is.

Bij twijfel print ndinfo() de locatie van de onderliggende buffer; twee arrays die hetzelfde adres rapporteren delen hun data. De volledige tabel view-versus-kopie staat op Views en kopieën.

6.19.4. Alloceer één keer, schrijf daarna

De grootste prestatievalkuil op de camera is het alloceren van nieuwe arrays binnen een lus die vele malen per seconde draait. Elke nieuwe ndarray vraagt de cam om RAM, en frequente nieuwe allocaties verspillen dat.

De meeste universele functies accepteren out= zodat het resultaat geschreven kan worden in een array die al bestaat:

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() accepteert buffer= om dezelfde reden; spectrogram() en de converters van het type from_int32_buffer() accepteren zowel out= als scratchpad=. Alloceer alles één keer en hergebruik het.

6.19.5. Gebruik in-place operatoren

b = b + 1 alloceert een tijdelijke buffer ter grootte van b, kopieert en wijst opnieuw toe. b += 1 wijzigt b rechtstreeks:

# makes a temporary
b = b + 1

# no temporary
b += 1

Hetzelfde idee geldt voor samengestelde expressies. a + b * c alloceert een tijdelijke buffer voor b * c. Het opsplitsen van de expressie in eenvoudige deeltoewijzingen die in een vooraf gealloceerde buffer schrijven elimineert de tijdelijke buffers:

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

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

6.19.6. Bouw het resultaat op, voeg er niet aan toe

ndarray heeft geen append – met opzet. Een array laten groeien zou betekenen dat er een nieuwe, grotere buffer wordt gealloceerd en de oude inhoud daarin wordt gekopieerd. Op een microcontroller alloceer je vooraf de uiteindelijke grootte en vul je deze:

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

Wanneer N werkelijk niet vooraf bekend is, schrijf dan naar een Python-list en converteer aan het einde één keer met array().

6.19.7. Slice-toewijzing in plaats van nieuwe arrays

Veel patronen van het type “bouw een nieuwe array uit stukken van andere” kunnen worden uitgedrukt als slice-toewijzingen in een vooraf gealloceerde buffer in plaats van een nieuwe allocatie bij elke aanroep.

Een rollend venster over een stroom samples – de basis van een voortschrijdend-gemiddeldefilter – is het schoolvoorbeeld. De buffer bevat de laatste N samples; elke iteratie laat de oudste vallen en voegt de nieuwste toe. De voor de hand liggende vorm herbouwt de buffer bij elke iteratie:

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

Dat is een nieuwe allocatie – en een kopie van N - 1 elementen – per sample. De slice-toewijzingsvorm schuift in-place:

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:] is de interessante regel: twee overlappende views in dezelfde buffer, waarbij de rechterkant-slice van het ene uiteinde wordt gelezen en naar het andere wordt geschreven. numpy doorloopt het onderliggende geheugen in de volgorde die de in-place verschuiving veilig maakt. Er wordt nooit een nieuwe array gealloceerd binnen de lus.

6.19.8. Pas op voor booleaanse maskers in streaminglussen

Booleaanse indexering en where() produceren bij elke aanroep een nieuwe array – de grootte van het resultaat hangt af van de data, dus geen enkele vooraf gealloceerde buffer kan de allocatie opvangen. Herhaald maskerbouwen in een streaminglus vult RAM met wegwerp-arrays. Een periodieke gc.collect() wint de ruimte terug:

import gc

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

Hetzelfde voorbehoud geldt voor samengestelde booleaanse expressies zoals (a > lo) & (a < hi) – elke operator alloceert een nieuwe bool-array. Wanneer een masker wordt hergebruikt, bouw het dan één keer op en bewaar het:

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