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