6.19. Продуктивність#

Ті самі архітектурні рішення, що роблять numpy швидким на камері – виклики бібліотечних функцій для цілих масивів, упаковані буфери з типізацією, представлення, що спільно використовують дані з джерелом – також формують набір звичок, про які варто знати. Сторінка Форма та кроки вже охопила правило розміщення по останній осі; ця сторінка каталогізує звички виділення пам’яті та dtype, що найбільше важать у потоковому циклі.

6.19.1. Обирайте розумний dtype#

Типовий dtype кожного конструктора – float. Для даних, що природньо є 8-бітними або 16-бітними – семплів ADC, пікселів зображень, показань датчиків – передавайте dtype= явно, вказуючи один із цілочисельних типів:

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

Економія RAM становить 2x для uint16 та 4x для uint8 порівняно з 4-байтовим типом float за замовчуванням. Математика також виконується швидше, оскільки цілочисельні шляхи коду всередині numpy є більш жорсткими, ніж узагальнені шляхи для float. Правило переповнення цілих чисел, розглянуте на сторінці Типи даних (dtype), застосовується – перед арифметикою, яка може переповнитися, виконуйте приведення до ширшого типу.

6.19.2. Надавайте перевагу ndarray над ітерованим об’єктом#

Більшість функцій редукції та універсальних функцій приймають як ітерований об’єкт, так і ndarray

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

Форма з ітерованим об’єктом змушує numpy проходити вхідні дані по одному Python-об’єкту за раз, перетворюючи кожен у число перш ніж він може його використати. З ndarray перетворення вже виконано, і виклик проходить прямо через упакований буфер.

Коли одні й ті самі дані використовуються більше одного разу, створіть ndarray один раз і передавайте його. Коли дані існують лише як список Python і споживаються один раз, вартість перетворення може перевищити прискорення – конструктор array() сам має обходити список та виділяти пам’ять.

6.19.3. Надавайте перевагу представленням над копіями#

Зрізи, індексування за однією віссю масиву вищого рангу, reshape(), transpose() та frombuffer() – усі повертають представлення, що спільно використовують дані з джерелом. Вони практично безкоштовні.

copy(), flatten(), булеве індексування (a[mask]) та будь-який арифметичний вираз виділяють копію. Вдавайтеся до них лише тоді, коли незалежний буфер дійсно необхідний.

Якщо є сумніви, ndinfo() виводить розташування базового буфера; два масиви, що повідомляють однакову адресу, спільно використовують дані. Повна таблиця представлення та копії знаходиться на Представлення та копії.

6.19.4. Виділяйте пам’ять один раз, потім записуйте#

Найбільший вузьке місце продуктивності на камері – виділення нових масивів усередині циклу, який виконується багато разів на секунду. Кожен новий ndarray запитує у камери RAM, а часті нові виділення витрачають її.

Більшість універсальних функцій приймають out=, щоб результат можна було записати у вже існуючий масив:

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() приймає buffer= з тієї самої причини; spectrogram() та конвертери у стилі from_int32_buffer() приймають і out=, і scratchpad=. Виділяйте все один раз і повторно використовуйте.

6.19.5. Використовуйте оператори «на місці»#

b = b + 1 виділяє тимчасовий масив розміром b, копіює та перепризначає. b += 1 модифікує b безпосередньо:

# makes a temporary
b = b + 1

# no temporary
b += 1

Та сама ідея застосовується до складених виразів. a + b * c виділяє тимчасовий масив для b * c. Розбивання виразу на прості підвирази, що записуються у попередньо виділений буфер, усуває тимчасові масиви:

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

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

6.19.6. Будуйте результат, не додавайте до нього по частинах#

ndarray не має append – навмисно. Розширення масиву означало б виділення нового, більшого буфера та копіювання старого вмісту до нього. На мікроконтролері заздалегідь виділяйте кінцевий розмір і заповнюйте його:

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

Коли N справді не відоме заздалегідь, записуйте у Python list і виконуйте одне перетворення наприкінці за допомогою array().

6.19.7. Присвоєння через зріз замість нових масивів#

Багато патернів «побудувати новий масив із частин інших» можна виразити як присвоєння через зріз у попередньо виділений буфер замість нового виділення при кожному виклику.

Ковзне вікно над потоком семплів – основа фільтра ковзного середнього – є канонічним прикладом. Буфер зберігає останні N семплів; кожна ітерація відкидає найстаріший і додає найновіший. Очевидна форма перебудовує буфер при кожній ітерації:

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

Це нове виділення – і копія N - 1 елементів – на кожен семпл. Форма з присвоєнням через зріз зсуває на місці:

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:] – це цікавий рядок: два перекриваючихся представлення того самого буфера, де правий зріз читається з одного кінця і записується в інший. numpy обходить базову пам’ять у порядку, що робить зсув на місці безпечним. Усередині циклу жодного нового масиву ніколи не виділяється.

6.19.8. Остерігайтеся булевих масок у потокових циклах#

Булеве індексування та where() створюють новий масив при кожному виклику – розмір результату залежить від даних, тому жоден попередньо виділений буфер не може поглинути виділення. Повторне побудування маски у потоковому циклі заповнює RAM одноразовими масивами. Виклик gc.collect() з певною частотою відновлює простір:

import gc

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

Те саме застереження стосується складених булевих виразів, як-от (a > lo) & (a < hi) – кожен оператор виділяє новий булевий масив. Коли маска повторно використовується, будуйте її один раз і зберігайте:

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