6.5. Форма и шаги (strides)

Данные внутри ndarray – это один упакованный блок чисел. Дескриптор перед этим блоком решает, как этот плоский блок считывается как тензор.

6.5.1. Что записывает дескриптор

Пять значений описывают, как читать блок данных как тензор:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)

a.ndim       # 2     - number of dimensions
a.shape      # (2, 3)- length along each dimension
a.itemsize   # 1     - bytes per element (from dtype)
a.size       # 6     - total number of elements
a.strides    # (3, 1)- step pattern through the buffer

Вспомогательная функция ndinfo() выводит их все, а также расположение лежащего в основе буфера за один вызов. Два массива, расположения буферов которых совпадают, разделяют память:

np.ndinfo(a)
# class: ndarray
# shape: (2, 3)
# strides: (3, 1)
# itemsize: 1
# data pointer: 0x...
# type: uint8

6.5.2. Объяснение шагов (strides)

Шаг (stride) – это число байтов, на которое нужно переместиться в блоке данных, чтобы сдвинуться на один элемент вдоль заданной оси. Для приведённого выше массива 2x3 типа uint8 шаги равны (3, 1): перемещение вниз на одну строку прыгает на 3 байта, перемещение вправо на один столбец прыгает на 1 байт. Это то же самое, что сказать, что строки хранятся вплотную друг к другу, слева направо:

memory: [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ]
          ^ row 0          ^ row 1
          <------- 3 bytes ---->

Чтобы прочитать a[i, j], numpy вычисляет i * strides[0] + j * strides[1] от начала блока данных и считывает оттуда itemsize байтов. Та же формула распространяется на любое число размерностей.

Это размещение – строки хранятся одна за другой, причём последняя ось меняется быстрее всего вдоль памяти – называется порядком по строкам (row-major). Каждый массив, который numpy выделяет на камере, использует это размещение.

6.5.3. У порядка по строкам есть последствия

Из «строки хранятся вплотную друг к другу» вытекают две вещи, которые важны при формировании буфера на камере.

Последняя ось непрерывна. Переход от a[0, 0] к a[0, 1] затрагивает следующий байт. Переход от a[0, 0] к a[1, 0] перепрыгивает через целую строку.

Последняя ось – быстрая ось для вычислений над массивом целиком. numpy на камере всегда обходит последнюю ось во внутреннем цикле, независимо от того, какая ось оказывается длиннее. Настольная библиотека numpy незаметно переупорядочивает свои циклы, чтобы поместить самую длинную ось во внутренний цикл; камера этого не делает, поэтому выбор размещения, который настольный numpy сгладил бы, здесь по-прежнему стоит времени. np.sum(m, axis=1) сворачивает последнюю ось и работает в непрерывном направлении; np.sum(m, axis=0) – нет. Когда у приложения есть выбор, как разместить буфер, ставьте длинную ось последней, чтобы операции вдоль неё оставались во внутреннем цикле.

Если размещение изначально неправильное, transpose() (или сокращение .T) исправляет его без копирования данных – просто меняя местами шаги:

a = b.T            # now iterates fast

Производительность содержит полное обсуждение производительности.

6.5.4. Reshape, transpose, срезы – правки дескриптора

Любая операция, которая только переписывает дескриптор, бесплатна. reshape меняет новые shape и strides поверх того же блока данных. transpose обращает шаги. a[::2] удваивает шаг. Каждая возвращает представление того же лежащего в основе буфера.

Всё, что должно обойти данные и записать новый буфер, является копией. Правило на данный момент таково: правки дескриптора бесплатны, а обходы данных – нет.

6.5.5. Замечание о ndim

numpy на камере собран с максимально поддерживаемым ndim, равным 4. Операции, которые произвели бы массив более высокого ранга, вызывают ValueError. Подавляющее большинство работы на стороне камеры является одномерной или двумерной, поэтому это ограничение редко составляет проблему.