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