Внутреннее устройство NumPy
- Объяснения кода NumPy на C
Внутренняя организация массивов 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 приведёт к неэффективному доступу к памяти, так как смежные элементы в сплющенном массиве (итераторе, на самом деле) не являются смежными в памяти.
Действительно, факт в том, что индексирование Python в списках и других последовательностях естественным образом приводит к внешнему к внутреннему порядку (первый индекс получает самую большую группу, следующий — следующий по величине, а последний получает наименьший элемент). Поскольку данные изображений обычно хранятся по строкам, это соответствует позиции внутри строк, являющейся последним индексируемым элементом.
Если вы хотите использовать порядок 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.10.1/reference/internals.html