6.19. Suorituskyky

Samat suunnitteluratkaisut, jotka tekevät numpy:stä nopean kamerassa – koko taulukkoa käsittelevät kirjastokutsut, pakatut tyypitetyt puskurit, lähdetietonsa kanssa jakavat näkymät – tuovat esiin myös joukon tapoja, jotka kannattaa tuntea. Sivu Muoto ja askelvälit käsitteli jo viimeisen akselin asettelusäännön; tämä sivu luetteloi varauksiin ja dtype-tyyppeihin liittyvät tavat, joilla on eniten merkitystä suoratoistosilmukassa.

6.19.1. Valitse järkevä dtype

Jokaisen konstruktorin oletus-dtype on float. Datalle, joka on luonnostaan 8-bittistä tai 16-bittistä – ADC-näytteet, kuvan pikselit, sensorin lukemat – välitä dtype= eksplisiittisesti johonkin kokonaislukutyyppiin:

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

RAM-säästö on 2-kertainen tyypillä uint16 ja 4-kertainen tyypillä uint8 verrattuna 4-tavuiseen float-oletukseen. Myös laskenta on nopeampaa, koska numpy:n sisäiset kokonaislukukoodipolut ovat tiiviimpiä kuin yleiset float-polut. Sivulla Dtype-tyypit käsitelty kokonaislukujen ylivuotosääntö pätee – muunna leveämpään tyyppiin ennen aritmetiikkaa, joka saattaa ylivuotaa.

6.19.2. Suosi ndarray-taulukkoa iteroitavan sijaan

Useimmat redusoinnit ja universaalit funktiot hyväksyvät joko iteroitavan tai ndarray-taulukon:

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

Iteroitava muoto pakottaa numpy:n käymään syötteen läpi yksi Python-olio kerrallaan, muuntaen kunkin luvuksi ennen kuin se voi käyttää sitä. ndarray-taulukkoa vastaan muunnos on jo tehty ja kutsu etenee suoraan pakatun puskurin läpi.

Kun samaa dataa käytetään useammin kuin kerran, rakenna ndarray kerran ja välitä sitä eteenpäin. Kun data on olemassa vain Python-listana ja se kulutetaan kerran, muunnoskustannus voi ylittää nopeushyödyn – array()-konstruktorin täytyy itse käydä lista läpi ja varata muistia.

6.19.3. Suosi näkymiä kopioiden sijaan

Viipalointi, korkeamman asteen taulukon yhden akselin indeksointi, reshape(), transpose() ja frombuffer() palauttavat kaikki näkymiä, jotka jakavat datan lähteen kanssa. Ne ovat käytännössä ilmaisia.

copy(), flatten(), totuusarvoindeksointi (a[mask]) ja mikä tahansa aritmeettinen lauseke varaavat kopion. Käytä niitä vain silloin, kun riippumatonta puskuria todella tarvitaan.

Epäselvissä tapauksissa ndinfo() tulostaa taustalla olevan puskurin sijainnin; kaksi samaa osoitetta raportoivaa taulukkoa jakavat datansa. Täydellinen näkymä-vs-kopio-taulukko on sivulla Näkymät ja kopiot.

6.19.4. Varaa kerran, kirjoita sitten

Yksittäinen suurin suorituskykyongelma kamerassa on uusien taulukoiden varaaminen silmukan sisällä, joka suoritetaan monta kertaa sekunnissa. Jokainen uusi ndarray pyytää kameralta RAM-muistia, ja tiheät uudet varaukset tuhlaavat sitä.

Useimmat universaalit funktiot hyväksyvät out=-argumentin, joten tulos voidaan kirjoittaa jo olemassa olevaan taulukkoon:

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() hyväksyy buffer=-argumentin samasta syystä; spectrogram() ja from_int32_buffer()-tyyliset muuntimet hyväksyvät sekä out= että scratchpad=. Varaa kaikki kerran ja käytä uudelleen.

6.19.5. Käytä paikalla muokkaavia operaattoreita

b = b + 1 varaa b:n kokoisen tilapäismuuttujan, kopioi ja sijoittaa uudelleen. b += 1 muokkaa b:tä suoraan:

# makes a temporary
b = b + 1

# no temporary
b += 1

Sama ajatus pätee yhdistettyihin lausekkeisiin. a + b * c varaa tilapäismuuttujan lausekkeelle b * c. Lausekkeen jakaminen yksinkertaisiin osasijoituksiin, jotka kirjoittavat esivarattuun puskuriin, poistaa tilapäismuuttujat:

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

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

6.19.6. Rakenna tulos, älä liitä siihen

ndarray:llä ei ole append-metodia – tarkoituksella. Taulukon kasvattaminen tarkoittaisi uuden, suuremman puskurin varaamista ja vanhan sisällön kopioimista siihen. Mikrokontrollerissa varaa lopullinen koko etukäteen ja täytä se:

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

Kun N ei todella ole etukäteen tiedossa, kirjoita Python-list:iin ja muunna kerran lopuksi funktiolla array().

6.19.7. Viipalesijoitus uusien taulukoiden sijaan

Monet ”rakenna uusi taulukko muiden palasista” -kuviot voidaan ilmaista viipalesijoituksina esivarattuun puskuriin sen sijaan, että jokaisella kutsulla tehtäisiin uusi varaus.

Liukuva ikkuna näytevirran yli – liukuvan keskiarvon suotimen perusta – on klassinen tapaus. Puskuri pitää viimeiset N näytettä; jokainen iteraatio pudottaa vanhimman ja liittää uusimman. Ilmeinen muoto rakentaa puskurin uudelleen joka iteraatiolla:

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

Tuo on uusi varaus – ja N - 1 alkion kopiointi – näytettä kohden. Viipalesijoitusmuoto siirtää paikallaan:

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:] on mielenkiintoinen rivi: kaksi päällekkäistä näkymää samaan puskuriin, oikean puolen viipale luettuna yhdestä päästä ja kirjoitettuna toiseen. numpy käy taustalla olevan muistin läpi siinä järjestyksessä, joka tekee paikalla tehtävästä siirrosta turvallisen. Silmukan sisällä ei koskaan varata uutta taulukkoa.

6.19.8. Varo totuusarvomaskeja suoratoistosilmukoissa

Totuusarvoindeksointi ja where() tuottavat uuden taulukon jokaisella kutsulla – tuloksen koko riippuu datasta, joten mikään esivarattu puskuri ei voi absorboida varausta. Toistuva maskien rakentaminen suoratoistosilmukassa täyttää RAM-muistin kertakäyttöisillä taulukoilla. Säännöllinen gc.collect() vapauttaa tilan:

import gc

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

Sama varoitus pätee yhdistettyihin totuusarvolausekkeisiin kuten (a > lo) & (a < hi) – jokainen operaattori varaa uuden bool-taulukon. Kun maskia käytetään uudelleen, rakenna se kerran ja säilytä:

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