Внутреннее устройство NumPy
- Объяснения кода NumPy на C
Внутренняя организация массивов NumPy
Понимание того, как массивы NumPy обрабатываются внутри, помогает лучше понять сам NumPy. В этом разделе мы не будем вдаваться в подробности. Те, кто желает получить полное понимание, могут обратиться к книге Travis Oliphant «Руководство по 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, отсюда и опции «C» и «FORTRAN» для порядка массива в NumPy.) Недостатком этого является потенциальное снижение производительности. Часто происходит последовательный доступ к данным, либо неявно в операциях с массивами, либо явно при циклическом переборе строк изображения. При этом данные будут доступны не оптимальным способом. При инкременте первого индекса на самом деле происходит доступ к элементам, расположенным далеко друг от друга в памяти, что обычно приводит к плохой производительности доступа к памяти. Например, для двумерного изображения «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 достаточно умен, чтобы при работе с универсальными функциями определить, какой индекс изменяется быстрее всего в памяти, и использовать это для внутреннего цикла. Поэтому для универсальных функций ни один из подходов не обладает существенными преимуществами в большинстве случаев. С другой стороны, использование .flat с массивом в порядке Fortran приведёт к неэффективному доступу к памяти, поскольку смежные элементы в сплющенном массиве (на самом деле итератор) не будут непрерывными в памяти.
Действительно, индексирование в Python для списков и других последовательностей естественным образом ведёт к порядку «снаружи внутрь» (первый индекс получает самую большую группу, следующий – следующую по величине, а последний – наименьший элемент). Так как данные изображений обычно хранятся построчно, это соответствует тому, что позиция внутри строки является последним индексируемым элементом.
END_OF_DOCUMENT_MARKERЕсли вы хотите использовать порядок Fortran, учтите, что существуют два подхода: 1) принять тот факт, что первый индекс не является самым быстро меняющимся в памяти, и переупорядочить данные во всех ваших I/O-процедурах при переходе из памяти на диск или наоборот, или использовать механизм NumPy для сопоставления первого индекса с наиболее быстро меняющимися данными. Мы рекомендуем первый подход, если это возможно. Недостаток второго подхода заключается в том, что многие функции NumPy будут возвращать массивы без порядка Fortran, если вы не позаботитесь о применении ключевого слова «order». Это было бы крайне неудобно.
В противном случае мы рекомендуем просто научиться изменять обычный порядок индексов при обращении к элементам массива. Конечно, это противоречит привычке, но это соответствует семантике Python и естественному порядку данных.
© 2005–2019 NumPy Developers
Licensed under the 3-clause BSD License.
https://docs.scipy.org/doc/numpy-1.14.5/reference/internals.html