Внутреннее устройство NumPy
Внутренняя организация массивов NumPy
Для лучшего понимания NumPy полезно немного разобраться в том, как обрабатываются массивы NumPy под капотом. В данном разделе не будут рассматриваться детали. Те, кто хочет узнать больше, обращаются к книге Трэвиса Олифанта «Руководство по NumPy».
Массивы NumPy состоят из двух основных компонентов: исходных данных массива (в дальнейшем будем называть буфером данных) и информации об исходных данных массива. Буфер данных — это то, что люди обычно представляют под массивами в C или Fortran: непрерывный (и фиксированный) блок памяти, содержащий элементы данных фиксированного размера. NumPy также содержит значительный набор данных, описывающих, как интерпретировать данные в буфере данных. Эта дополнительная информация содержит (в числе прочего):
- Размер базового элемента данных в байтах
- Начальная позиция данных в буфере данных (смещение относительно начала буфера данных).
- Количество измерений и размер каждого измерения
- Отступы между элементами для каждого измерения («шаг»). Он не обязательно должен быть кратным размеру элемента
- Порядок байтов данных (который может отличаться от родного)
- Является ли буфер только для чтения
- Информация (через объект dtype) об интерпретации базового элемента данных. Базовый элемент может быть простым целым или вещественным числом, или это может быть составной объект (например, похожий на структуру), фиксированное поле символов или указатели на объекты Python.
- Представление массива как C-порядка или Fortran-порядка.
Эта организация позволяет очень гибко использовать массивы. Она позволяет легко изменять метаданные для изменения интерпретации буфера массива. Изменение порядка байтов массива — это простое изменение, не требующее переупорядочения данных. Размер массива может быть изменён очень легко, без изменения буфера данных или копирования данных.
В числе прочего это позволяет создать новый объект метаданных массива, использующий тот же буфер данных, чтобы создать новое представление этих данных, имеющее другое представление буфера (например, другой размер, смещение, порядок байтов, шаги и т. д.), но использующее те же байты данных. Многие операции в NumPy делают именно это, например, срезы.
В большинстве случаев эти новые версии метаданных массива, использующие тот же буфер данных, являются новыми «представлениями» буфера данных. Существует другой объект ndarray, но он использует тот же буфер данных. Вот почему для создания независимой копии буфера данных необходимо принудительно создавать копии с помощью метода .copy().
Новые представления массивов означают, что счётчик ссылок объекта буфера данных увеличивается. Простое удаление исходного объекта массива не удалит буфер данных, если другие представления его всё ещё существуют.
Вопросы порядка индексирования многомерных массивов
Как правильно индексировать многомерные массивы? Прежде чем прийти к однозначному выводу, как индексировать многомерные массивы, важно понять, почему этот вопрос вызывает путаницу. В этом разделе мы постараемся подробно объяснить, как работает индексирование в NumPy, и почему мы используем принятую конвенцию для изображений, а также когда может быть целесообразно использовать другие конвенции.
В первую очередь, важно понять, что существует две противоречивых конвенции индексирования двумерных массивов. Матричная нотация использует первый индекс для указания строки, а второй индекс — для указания столбца. Это противоположно геометрически ориентированной конвенции для изображений, где обычно считается, что первый индекс представляет x-координату (т.е. столбец), а второй — y-координату (т.е. строку). Это само по себе является источником множества путаниц; пользователи, ориентированные на матрицы, и пользователи, ориентированные на изображения, ожидают разного поведения при индексировании.
Вторая проблема заключается в том, как индексы соответствуют порядку хранения элементов в памяти. В Fortran первый индекс изменяется быстрее всего при прохождении элементов двумерного массива при хранении в памяти. Если вы используете матричную конвенцию индексирования, это означает, что матрица хранится по столбцам (поскольку при изменении первого индекса переходит к следующей строке). Поэтому Fortran считается языком со столбцовой главной записью. C имеет обратную конвенцию. В C последний индекс изменяется быстрее всего при прохождении массива в памяти. Следовательно, C является языком с главной записью по строкам. Матрица хранится по строкам. Обратите внимание, что в обоих случаях предполагается использование матричной конвенции индексирования, т. е. и в Fortran, и в C первый индекс — это строка. Обратите внимание, что эта конвенция подразумевает, что конвенция индексирования неизменна, а порядок данных изменяется для её сохранения.
Но это не единственный способ взглянуть на проблему. Представьте, что у вас есть большие двумерные массивы (изображения или матрицы), хранящиеся в файлах данных. Предположим, что данные хранятся по строкам, а не по столбцам. Если мы хотим сохранить нашу конвенцию индексирования (матричную или для изображений), это означает, что в зависимости от используемого языка, нам, возможно, придётся переупорядочить данные, если они считываются в память, чтобы сохранить нашу конвенцию индексирования. Например, если мы считываем данные, упорядоченные по строкам, в память без переупорядочения, это будет соответствовать матричной конвенции индексирования для C, но не для Fortran. И наоборот, это будет соответствовать конвенции индексирования для изображений в Fortran, но не для C. Для C, если используются данные, хранящиеся в порядке строк, и необходимо сохранить конвенцию индексирования для изображений, данные должны быть переупорядочены при считывании в память.
В конечном счёте, то, что вы делаете для Fortran или C, зависит от того, что важнее: не переупорядочивать данные или сохранять конвенцию индексирования. Для больших изображений переупорядочение данных может быть дорогостоящим, и часто конвенция индексирования инвертируется, чтобы избежать этого.
В NumPy ситуация ещё более сложная. Внутренняя инфраструктура массивов NumPy достаточно гибкая, чтобы принимать любой порядок индексов. Можно просто переупорядочить индексы, изменив внутренние сведения о шагах массивов, не переупорядочивая сами данные. NumPy будет знать, как сопоставить новый порядок индексов с данными без перемещения данных.
Так если это так, то почему не выбрать порядок индексов, который лучше всего соответствует вашим ожиданиям? В частности, почему бы не определить изображения в порядке строк, чтобы использовать конвенцию для изображений? (Это иногда называется конвенцией Fortran по сравнению с конвенцией C, поэтому в NumPy есть опции «C» и «FORTRAN» для упорядочивания массивов). Недостаток этого — потенциальные потери производительности. Обычно данные обращаются последовательно, либо неявно в операциях с массивами, либо явно с помощью циклов по строкам изображения. Когда это делается, элементы, расположенные далеко в памяти, будут обращаться последовательно, что, как правило, снижает скорость доступа к памяти. Например, для двумерного изображения «im», определённого так, что im[0, 10] представляет значение в точке x=0, y=10. Для согласованности с обычным поведением Python, im[0] должно было бы представлять столбец в точке x=0. Однако эти данные были бы распределены по всему массиву, поскольку данные хранятся в порядке строк. Несмотря на гибкость индексирования NumPy, оно не может полностью устранить тот факт, что базовые операции становятся неэффективными из-за порядка данных, или то, что получение непрерывных подмассивов по-прежнему неудобно (например, im[:,0] для первой строки, а не im[0]), поэтому нельзя использовать идиому вида for row in im; for col in im. Хотя это работает, но не даёт непрерывных данных столбцов.
Как оказалось, NumPy достаточно умный при работе с ufuncs, чтобы определить, какой индекс изменяется быстрее всего в памяти, и использовать его для внутреннего цикла. Таким образом, для ufuncs в большинстве случаев нет большого внутреннего преимущества ни одного из подходов. С другой стороны, использование .flat с массивом в порядке FORTRAN приведёт к неэффективному доступу к памяти, поскольку соседние элементы в сплющенном массиве (итератор, на самом деле) не являются непрерывными в памяти.
END_OF_DOCUMENT_MARKERДействительно, факт заключается в том, что индексация Python в списках и других последовательностях естественным образом приводит к порядку «снаружи вовнутрь» (первый индекс получает самую большую группу, следующий — следующую по величине, а последний — наименьший элемент). Поскольку данные изображения обычно хранятся по строкам, это соответствует тому, что позиция внутри строки является последним индексируемым элементом.
Если вы всё же хотите использовать порядок Fortran, имейте в виду два подхода: 1) принять тот факт, что первый индекс просто не меняется в памяти наиболее быстро и переупорядочить все ваши I/O-процедуры, переходя от памяти к диску или наоборот, или использовать механизм numpy для сопоставления первого индекса с наиболее быстро меняющимися данными. Мы рекомендуем первый подход, если это возможно. Недостаток второго подхода заключается в том, что многие функции numpy дадут массивы без порядка Fortran, если вы не позаботитесь использовать ключевое слово «order». Это будет очень неудобно.
В противном случае мы рекомендуем просто научиться изменять обычный порядок индексов при обращении к элементам массива. Конечно, это противоречит интуиции, но это лучше соответствует семантике Python и естественному порядку данных.
© 2005–2021 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.20/reference/internals.html