Spec-Zone.ru › NumPy 1.18

Объяснения кода NumPy на C

Фанатизм заключается в удвоении усилий, когда вы забыли свою цель. — Джордж Сантаяна

Авторитет — это человек, который может рассказать вам больше о чём-то, чем вы действительно хотите знать. — Неизвестный

В этой главе мы пытаемся объяснить логику некоторых новых фрагментов кода. Цель этих объяснений — позволить кому-либо понять идеи, лежащие в основе реализации, несколько легче, чем просто рассматривать код. Возможно, таким образом, алгоритмы смогут быть улучшены, заимствованы и/или оптимизированы больше людей.

Модель памяти

Один из фундаментальных аспектов ndarray заключается в том, что массив рассматривается как «блок» памяти, начинающийся с некоторого местоположения. Интерпретация этой памяти зависит от информации о шаге. Для каждой размерности в массиве размерности N целое число (шаг) диктует, сколько байтов нужно пропустить, чтобы получить следующий элемент в этой размерности. Если у вас не массив с одним сегментом, эта информация о шаге должна учитываться при прохождении по массиву. Несложно написать код, принимающий шаги, вам просто нужно использовать указатели (char *), потому что шаги измеряются в байтах. Имейте в виду также, что шаги не обязательно должны быть кратны размеру элемента. Также помните, что если число размерностей массива равно 0 (иногда называется массивом ранга 0), то переменные шаги и размерности равны NULL.

Помимо структурной информации, содержащейся в членах шаги и размерности объекта PyArrayObject, флаги содержат важную информацию о том, как можно получить доступ к данным. В частности, флаг NPY_ARRAY_ALIGNED устанавливается, когда память находится на подходящей границе в соответствии с типом данных массива. Даже если у вас есть непрерывный блок памяти, нельзя просто предположить, что безопасно дереференцировать указатель, специфичный для типа данных, на элемент. Только если установлен флаг NPY_ARRAY_ALIGNED, эта операция безопасна (на некоторых платформах она будет работать, но на других, таких как Solaris, это приведёт к ошибке доступа к памяти). Также следует убедиться, что установлен флаг NPY_ARRAY_WRITEABLE, если вы планируете запись в область памяти массива. Также можно получить указатель на область памяти, к которой нельзя писать. Иногда запись в область памяти, когда флаг NPY_ARRAY_WRITEABLE не установлен, просто неуместно. В других случаях это может привести к сбоям программы (например, область данных — это файл с отображением памяти только для чтения).

Инкапсуляция типа данных

Тип данных — важная абстракция ndarray. Операции обращаются к типу данных, чтобы обеспечить необходимую функциональность для работы с массивом. Эта функциональность предоставляется в списке указателей функций, на которые указывает член ‘f’ структуры PyArray_Descr. Таким образом, количество типов данных можно расширить, просто предоставив структуру PyArray_Descr с соответствующими указателями функций в члене ‘f’. Для встроенных типов есть некоторые оптимизации, которые обходят этот механизм, но суть абстракции типа данных — позволить добавлять новые типы данных.

Один из встроенных типов данных, тип данных void, позволяет использовать произвольные структурированные типы, содержащие 1 или более полей в качестве элементов массива. Поле — это просто другой объект типа данных вместе со смещением в текущем структурированном типе. Для поддержки произвольно вложенных полей реализованы несколько рекурсивных реализаций доступа к типу данных для типа void. Распространённым приёмом является проход по элементам словаря и выполнение определённой операции на основе объекта типа данных, хранящегося в заданном смещении. Эти смещения могут быть произвольными числами. Поэтому необходимо учитывать возможность столкновения с невыровненными данными, если это необходимо.

N-мерные итераторы

Очень распространённой операцией во многих кодах NumPy является необходимость перебора всех элементов общего N-мерного массива со смещениями. Эта операция общего N-мерного цикла абстрагирована в понятии объекта-итератора. Чтобы написать N-мерный цикл, вам нужно только создать объект-итератор из ndarray, работать с членом dataptr структуры объекта-итератора и вызвать макрос PyArray_ITER_NEXT (it) на объекте-итераторе, чтобы перейти к следующему элементу. «Следующий» элемент всегда в C-непрерывном порядке.

Макрос работает, сначала обрабатывая случаи C-непрерывности, 1-D и 2-D, которые работают очень просто.

В общем случае итерация работает путём отслеживания списка счётчиков координат в объекте-итераторе. На каждой итерации последний счётчик координат увеличивается (начиная с 0). Если этот счётчик меньше чем на единицу меньше размера массива в этой размерности (предварительно вычисленное и хранящееся значение), то счётчик увеличивается, член dataptr увеличивается на смещения в этой размерности, и макрос завершается. Если достигается конец размерности, счётчик последней размерности обнуляется, а dataptr перемещается в начало этой размерности путём вычитания значения смещения, умноженного на на единицу меньше числа элементов в этой размерности (это также предварительно вычислено и хранится в члене backstrides объекта-итератора). В этом случае макрос не завершается, но счётчик локальной размерности уменьшается, так что предпоследняя размерность принимает роль, которую играла последняя размерность, и описанные выше тесты снова выполняются для предпоследней размерности. Таким образом, dataptr корректируется соответствующим образом для произвольных смещений.

Член coordinates структуры PyArrayIterObject сохраняет текущий N-мерный счётчик, если только базовый массив не является C-непрерывным, в этом случае подсчёт координат пропускается. Член index объекта PyArrayIterObject отслеживает текущий плоский индекс итератора. Он обновляется макросом PyArray_ITER_NEXT.

Вещание

В Numeric, предшественнике Numpy, вещание было реализовано в нескольких строках кода, глубоко закопанных в ufuncobject.c. В NumPy понятие вещания было абстрагировано, чтобы его можно было выполнять в нескольких местах. Вещание обрабатывается функцией PyArray_Broadcast. Эта функция требует, чтобы был передан объект PyArrayMultiIterObject (или что-то, что является бинарным эквивалентом). PyArrayMultiIterObject отслеживает число размерностей вещания и размер в каждой размерности вместе с общим размером результата вещания. Он также отслеживает количество передаваемых массивов и указатель на итератор для каждого из передаваемых массивов.

Функция PyArray_Broadcast принимает уже определённые итераторы и использует их для определения формы вещания в каждой размерности (чтобы создать итераторы одновременно с вещанием, используйте функцию PyMultiIter_New). Затем итераторы корректируются так, чтобы каждый итератор думал, что итерирует по массиву с размером вещания. Это делается путём корректировки числа размерностей итераторов и формы в каждой размерности. Это работает, потому что шаги итератора также корректируются. Вещание корректирует (или добавляет) только размерности длиной 1. Для этих размерностей переменная шаги просто устанавливается в 0, так что указатель данных для итератора по этому массиву не перемещается, когда операция вещания работает над расширенной размерностью.

В Numeric вещание всегда реализовывалось с помощью смещений со значением 0 для расширенных размерностей. В NumPy это делается точно так же. Существенная разница заключается в том, что теперь массив смещений отслеживается в PyArrayIterObject, итераторы, участвующие в результате вещания, отслеживаются в PyArrayMultiIterObject, и вызов PyArray_BroadCast реализует правила вещания.

Массивные скаляры

Массивные скаляры предлагают иерархию типов Python, которые позволяют взаимно однозначное соответствие между типом данных, хранящимся в массиве, и типом Python, возвращаемым при извлечении элемента из массива. Исключение сделано для массивов объектов. Массивы объектов — это разнородные коллекции произвольных объектов Python. Когда вы выбираете элемент из массива объектов, вы получаете обратно исходный объект Python (а не скаляр массива объектов, который существует, но редко используется для практических целей).

Массивные скаляры также предлагают те же методы и атрибуты, что и массивы, с целью, чтобы тот же код мог использоваться для поддержки произвольных размерностей (включая 0-мерные). Массивные скаляры являются неизменяемыми (только для чтения), за исключением скаляра void, к которому также можно записывать, чтобы работа с полями структурированных массивов была более естественной (a[0]['f1'] = value).

Индексирование

Все операции индексирования Python arr[index] организованы путём предварительной подготовки индекса и определения типа индекса. Поддерживаемые типы индексов:

  • целое число
  • newaxis
  • срез
  • ellipsis
  • массивы/подобные массивы целых чисел (fancy)
  • булевы (один булев массив); если в качестве индекса используется более одного булева массива или форма не совпадает точно, булев массив преобразуется в массив целых чисел.
  • 0-мерные булевы (и также целые числа); 0-мерные булевы массивы являются особым случаем, который должен быть обработан в коде продвинутого индексирования. Они сигнализируют о том, что 0-мерный булев массив должен был интерпретироваться как массив целых чисел.

А также особый случай скалярного массива, сигнализирующий о том, что массив целых чисел был интерпретирован как целочисленный индекс, что важно, потому что целочисленный индекс массива требует копирования, но игнорируется, если возвращается скаляр (полный целочисленный индекс). Подготовленный индекс гарантируется как валидный, за исключением значений, выходящих за пределы границ, и ошибок широковещательной рассылки для продвинутого индексирования. Это включает добавление многоточия для неполных индексов, например, когда двумерный массив индексируется одним целым числом.

Следующий шаг зависит от типа найденного индекса. Если все измерения индексируются целым числом, возвращается или устанавливается скаляр. Массив с единственным булевым индексированием вызовет специализированные булевы функции. Индексы, содержащие многоточие или срез, но без продвинутого индексирования, всегда создают представление в старом массиве путем расчета новых шагов и смещения памяти. Это представление затем может быть возвращено или, для присваивания, заполнено с помощью PyArray_CopyObject. Обратите внимание, что PyArray_CopyObject также может быть вызван для временных массивов в других ветвях, чтобы поддерживать сложные присваивания, когда массив имеет тип объекта.

Продвинутое индексирование

Наиболее сложным случаем является продвинутое индексирование, которое может или не может быть комбинировано с типичным индексированием на основе представления. Здесь целочисленные индексы интерпретируются как основанные на представлении. Прежде чем пытаться понять это, вы можете ознакомиться с его тонкостями. Код продвинутого индексирования имеет три разные ветви и один специальный случай:

  • Существует один индексный массив, и он, а также массив присваивания, может быть итерирован тривиально. Например, они могут быть смежными. Также массив индексов должен быть типа intp и массив значений в операциях присваивания должен быть правильного типа. Это чисто быстрый путь.
  • Существуют только целочисленные индексы массива, так что подмассива не существует.
  • Индексирование на основе представления и продвинутое индексирование смешаны. В этом случае индексирование на основе представления определяет набор подмассивов, которые объединяются продвинутым индексированием. Например, arr[[1, 2, 3], :] создается путем вертикального объединения подмассивов arr[1, :], arr[2,:], и arr[3, :].
  • Существует подмассив, но он имеет ровно один элемент. Этот случай можно обработать так, как будто подмассива нет, но при настройке требуется определенная осторожность.

Решение, какой случай применим, проверка широковещательной рассылки и определение необходимого типа транспонирования выполняются в PyArray_MapIterNew. После настройки есть два случая. Если нет подмассива или он содержит только один элемент, итерация по подмассивам не нужна, и подготавливается итератор, который итерирует все индексные массивы а также массив результатов или значений. Если подмассив существует, подготавливаются три итератора. Один для индексных массивов, один для массива результатов или значений (без его подмассива) и один для подмассивов исходного и массива результатов/присваивания. Первые два итератора дают (или позволяют вычислить) указатели на начало подмассива, что позволяет перезапустить итерацию по подмассивам.

Когда продвинутые индексы находятся рядом друг с другом, может потребоваться транспонирование. Все необходимые транспонирования обрабатываются PyArray_MapIterSwapAxes и должны обрабатываться вызывающей стороной, если PyArray_MapIterNew не запрошено выделить результат.

После подготовки получение и установка относительно просты, хотя необходимо учитывать различные режимы итерации. Если при получении элемента существует только один индексный массив, то предварительно проверяется валидность индексов. В противном случае это обрабатывается в самом внутреннем цикле для оптимизации.

Универсальные функции

Универсальные функции — это вызываемые объекты, которые принимают N входные данные и производят M выходные данные, обёртывая базовые циклы по одному измерению, работающие по элементу, в полные, простые в использовании функции, которые бесшовно реализуют широковещательную рассылку, проверку типов и буферизованное приведение типов, а также обработку аргументов результата. Новые универсальные функции обычно создаются на C, хотя существует механизм создания ufunc из функций Python (frompyfunc). Пользователь должен предоставить цикл по одному измерению, который реализует основную функцию, принимающую скалярные значения входных данных и помещающий результирующие скаляры в соответствующие слоты выходных данных, как описано в реализации.

Настройка

Каждый вызов ufunc влечет за собой некоторые накладные расходы, связанные с настройкой вычислений. Практическое значение этих накладных расходов заключается в том, что, хотя фактическое вычисление ufunc очень быстро, вы сможете написать код, специфичный для массивов и типов, который будет работать быстрее для малых массивов, чем ufunc. В частности, использование ufunc для выполнения многих вычислений над 0-мерными массивами будет медленнее, чем другие решения на основе Python (модуль scalarmath, импортируемый без явного указания, существует именно для того, чтобы дать скалярам массивов вид и ощущение вычислений на основе ufunc с значительно меньшими накладными расходами).

При вызове ufunc необходимо выполнить много действий. Информация, собранная в ходе этих операций настройки, хранится в объекте цикла. Этот объект цикла представляет собой структуру C (которая может стать объектом Python, но не инициализируется как таковой, потому что используется только внутри). Этот объект цикла имеет структуру, необходимую для использования с PyArray_Broadcast, чтобы обработка широковещательной рассылки выполнялась так же, как и в других разделах кода.

Первым делом из глобального словаря, специфичного для потока, извлекаются текущие значения для размера буфера, маски ошибок и связанного объекта ошибки. Состояние маски ошибок определяет, что произойдет, когда обнаружится ошибка. Следует отметить, что проверка флагов ошибок аппаратного обеспечения выполняется только после выполнения каждого цикла по одному измерению. Это означает, что если входные и выходные массивы являются смежными и правильного типа, так что выполняется один цикл по одному измерению, то флаги могут не проверяться до тех пор, пока не будут вычислены все элементы массива. Поиск этих значений в словаре, специфичном для потока, занимает время, которое легко игнорировать для всех, кроме очень малых массивов.

После проверки потоковых глобальных переменных входные данные оцениваются, чтобы определить, как ufunc должен продолжить работу, и входные и выходные массивы создаются при необходимости. Любые входные данные, которые не являются массивами, преобразуются в массивы (с использованием контекста при необходимости). Отмечается, какие из входных данных являются скалярами (и поэтому преобразуются в 0-мерные массивы).

Далее выбирается соответствующий цикл по одному измерению из доступных циклов по одному измерению для ufunc на основе типов входных массивов. Этот цикл по одному измерению выбирается путем попытки сопоставить сигнатуру типов данных входных данных с доступными сигнатурами. Сигнатуры, соответствующие встроенным типам, хранятся в члене types структуры ufunc. Сигнатуры, соответствующие определенным пользователем типам, хранятся в связанном списке информации о функциях, где головной элемент хранится как CObject в словаре userloops с ключом номер типа данных (первый тип данных, определенный пользователем, в списке аргументов, используется в качестве ключа). Сигнатуры ищутся до тех пор, пока не будет найдена сигнатура, в которую все входные массивы могут быть безопасно преобразованы (игнорируя любые скалярные аргументы, которые не допускаются для определения типа результата). Следствием этой процедуры поиска является то, что «менее важные типы» должны располагаться ниже «более важных типов», когда сигнатуры хранятся. Если цикл по одному измерению не найден, сообщается об ошибке. В противном случае список аргументов обновляется сохраненной сигнатурой — в случае необходимости преобразования и для исправления типов результатов, предполагаемых циклом по одному измерению.

Если у ufunc 2 входа и 1 выход, и второй вход — массив объектов, выполняется проверка специального случая, чтобы вернуть NotImplemented, если второй вход не является ndarray, не имеет атрибута __array_priority__ и не имеет специального метода __r{op}__. Таким образом, Python получает сигнал, чтобы дать другому объекту возможность выполнить операцию вместо использования общих вычислений массивов объектов. Это позволяет (например) разреженным матрицам переопределять оператор умножения циклом по одному измерению.

Для входных массивов, меньших указанного размера буфера, делаются копии всех несмежных, невыровненных или несовпадающих по порядку байтов массивов, чтобы обеспечить использование одного цикла для малых массивов. Затем создаются итераторы массивов для всех входных массивов, и полученное множество итераторов передается в единую форму.

Затем обрабатываются выходные аргументы (если таковые имеются), и создаются любые отсутствующие выходные массивы. Если какой-либо предоставленный выходной массив не имеет правильный тип (или не выровнен) и меньше размера буфера, то новый выходной массив создается со специальным флагом WRITEBACKIFCOPY. В конце функции вызывается PyArray_ResolveWritebackIfCopy, чтобы его содержимое было скопировано обратно в выходной массив. Затем обрабатываются итераторы выходных аргументов.

Наконец, принимается решение о том, как выполнить механизм цикла, чтобы гарантировать, что все элементы входных массивов объединяются для создания выходных массивов правильного типа. Варианты выполнения цикла — однократный цикл (для смежных, выровненных и правильного типа данных), цикл с шагом (для несмежных, но все еще выровненных и правильного типа данных) и буферизованный цикл (для невыровненных или неправильного типа данных). В зависимости от вызываемого метода выполнения цикл затем настраивается и вычисляется.

Вызов функции

В этом разделе описывается, как настраивается и выполняется базовый цикл вычисления универсальной функции для каждого из трех различных типов выполнения. Если NPY_ALLOW_THREADS определено во время компиляции, то до вызова циклов, пока не задействованы массивы объектов, освобождается глобальная блокировка интерпретатора Python (GIL). При необходимости она восстанавливается для обработки условий ошибки. Флаги аппаратных ошибок проверяются только после завершения цикла по одному измерению.

Один цикл

Это самый простой случай. Ufunc выполняется путем вызова базового цикла по одному измерению ровно один раз. Это возможно только в том случае, если данные выровнены и правильного типа (включая порядок байтов) для входных и выходных данных, и все массивы имеют одинаковые шаги (смежные, 0-мерные или 1-мерные). В этом случае цикл вычисления по одному измерению вызывается один раз для вычисления вычисления для всего массива. Обратите внимание, что флаги аппаратных ошибок проверяются только после завершения всех вычислений.

Цикл с шагом

Когда входные и выходные массивы выровнены и правильного типа, но шаги не одинаковы (несмежные и 2-мерные или более), для вычислений используется вторая структура цикла. Этот подход преобразует все итераторы входных и выходных аргументов в итераторы по всем измерениям, кроме самого большого. Внутренний цикл затем обрабатывается базовым циклом по одному измерению. Внешний цикл — это стандартный цикл итератора по преобразованным итераторам. Флаги аппаратных ошибок проверяются после завершения каждого цикла по одному измерению.

Буферизованный цикл

Данный код обрабатывает ситуации, когда входные и/или выходные массивы не выровнены или имеют неправильный тип данных (включая переставленные байты), по сравнению с тем, что ожидает внутренний цикл по одному измерению. Также предполагается, что массивы не являются непрерывными. Код очень похож на код цикла с шагом, за исключением того, что внутренний цикл по одному измерению модифицирован таким образом, что предварительная обработка выполняется над входными данными, а последующая обработка — над выходными данными в блоках по bufsize элементов (где bufsize — параметр, задаваемый пользователем). Внутренний вычислительный цикл по одному измерению вызывается для скопированных данных (если это необходимо). Код настройки и код цикла в этом случае значительно сложнее, поскольку он должен обрабатывать:

  • выделение памяти для временных буферов
  • определение, следует ли использовать буферы для входных и выходных данных (невыровненные данные и/или неправильный тип данных)
  • копирование и, возможно, приведение типов данных для всех входных или выходных данных, для которых необходимы буферы.
  • специальную обработку массивов типа Object, чтобы правильно обрабатывать счётчики ссылок при копировании и/или приведении типов.
  • разбиение внутреннего цикла по одному измерению на блоки по bufsize элементов (с возможным остатком).

Опять же, флаги ошибок оборудования проверяются в конце каждого цикла по одному измерению.

Конечная обработка выходных данных

Ufuncs позволяют без проблем передавать другие массивоподобные классы через интерфейс в том смысле, что входные данные определённого класса приведут к тому, что выходные данные будут иметь тот же класс. Механизм работы следующий. Если любой из входных данных не является ndarray и определяет метод __array_wrap__, то класс с наибольшим атрибутом __array_priority__ определяет тип всех выходных данных (за исключением любых передаваемых выходных массивов). Метод __array_wrap__ входного массива будет вызван с ndarray, возвращаемым ufunc, в качестве входного аргумента. Поддерживаются два стиля вызова функции __array_wrap__. Первый принимает ndarray в качестве первого аргумента и кортеж «контекста» во втором аргументе. Контекст — это (ufunc, аргументы, номер выходного аргумента). Это первый вызов, который будет выполнен. Если возникает ошибка TypeError, то функция вызывается только с ndarray в качестве первого аргумента.

Методы

Существует три метода ufuncs, требующие вычислений, аналогичных универсальным ufuncs. Это reduce, accumulate и reduceat. Каждый из этих методов требует команды настройки, за которой следует цикл. Существует четыре возможных стиля циклов для методов, соответствующие отсутствию элементов, одному элементу, циклу с шагом и буферизованному циклу. Это те же самые базовые стили циклов, реализованные для вызова универсальной функции, за исключением случаев с нулевым и одним элементом, которые являются специальными случаями, возникающими, когда объекты входного массива имеют 0 и 1 элемент соответственно.

Настройка

Функция настройки для всех трёх методов — construct_reduce. Эта функция создаёт объект цикла с сокращением и заполняет его параметрами, необходимыми для завершения цикла. Все методы работают только с ufuncs, которые принимают 2 входа и возвращают 1 выход. Следовательно, внутренний цикл по одному измерению выбирается при предположении подписи [ otype, otype, otype ], где otype — запрашиваемый тип данных для сокращения. Размер буфера и обработка ошибок затем извлекаются из глобального хранилища (по потоку). Для небольших массивов, которые не выровнены или имеют неправильный тип данных, создаётся копия, чтобы использовать небуферизованный фрагмент кода. Затем выбирается стратегия циклирования. Если в массиве 1 элемент или 0 элементов, то выбирается простой метод циклирования. Если массив не невыровнен и имеет правильный тип данных, то выбирается циклирование с шагом. В противном случае необходимо выполнить буферизованное циклирование. Параметры циклирования устанавливаются, и создаётся возвращаемый массив. Форма выходного массива отличается в зависимости от того, является ли метод reduce, accumulate или reduceat. Если выходной массив уже задан, то проверяется его форма. Если выходной массив не является C-непрерывным, невыровненным и не имеет правильного типа данных, то создаётся временная копия с установленным флагом WRITEBACKIFCOPY. Таким образом, методы смогут работать с хорошо себя ведущим выходным массивом, но результат будет скопирован обратно в фактический выходной массив, когда вызывается PyArray_ResolveWritebackIfCopy по завершении функции. Наконец, создаются итераторы для циклирования по нужному осям (в зависимости от значения оси, переданного методу), и процедура настройки возвращается к фактической вычислительной процедуре.

Reduce

Все методы ufunc используют те же самые внутренние циклы вычислений по одному измерению с входными и выходными аргументами, скорректированными таким образом, чтобы выполнялось соответствующее сокращение. Например, ключевым моментом работы reduce является то, что цикл по одному измерению вызывается с выходом и вторым входом, указывающими на одно и то же положение в памяти, и оба имеют шаг 0. Первый вход указывает на входной массив со значением шага, задаваемого соответствующим шагом для выбранной оси. Таким образом, выполняемая операция —

\begin{align*}
o & = & i[0] \\
o & = & i[k]\textrm{<op>}o\quad k=1\ldots N
\end{align*}

где N+1 — количество элементов во входном массиве, i, o — выход, а i[k] — k^{\textrm{th}} элемент i вдоль выбранной оси. Эта базовая операция повторяется для массивов с большей, чем 1, размерностью, так что сокращение выполняется для каждого одномерного подмассива вдоль выбранной оси. Итератор, у которого удалена выбранная размерность, обрабатывает это циклирование.

Для буферизованных циклов необходимо позаботиться о копировании и приведении типов данных перед вызовом функции цикла, потому что внутренний цикл ожидает выровненные данные правильного типа данных (включая порядок байтов). Буферизованный цикл должен выполнить это копирование и приведение типов перед вызовом функции цикла для блоков, не превышающих заданный пользователем bufsize.

Accumulate

Функция accumulate очень похожа на функцию reduce в том, что выход и второй вход оба указывают на выход. Разница в том, что второй вход указывает на память на один шаг позади текущего указателя выхода. Таким образом, выполняемая операция —

\begin{align*}
o[0] & = & i[0] \\
o[k] & = & i[k]\textrm{<op>}o[k-1]\quad k=1\ldots N.
\end{align*}

Выход имеет ту же форму, что и вход, и каждый цикл по одному измерению выполняется над N элементами, когда форма по выбранной оси равна N+1. Опять же, буферизованные циклы позаботятся о копировании и приведении типов данных перед вызовом внутреннего вычислительного цикла по одному измерению.

Reduceat

Функция reduceat является обобщением функций reduce и accumulate. Она реализует сокращение по диапазонам входного массива, указанным индексами. Дополнительный аргумент индексов проверяется, чтобы убедиться, что каждый вход не слишком велик для входного массива вдоль выбранного измерения, прежде чем будут выполнены вычисления цикла. Реализация цикла обрабатывается кодом, очень похожим на код reduce, повторяемый столько раз, сколько элементов в входном индексе. В частности: указатель первого входа, переданный внутреннему вычислительному циклу по одному измерению, указывает на входной массив в правильном месте, указанном массивом индексов. Кроме того, указатель выхода и указатель второго входа, переданные внутреннему циклу по одному измерению, указывают на одно и то же место в памяти. Размер цикла вычислений по одному измерению фиксируется как разность между текущим индексом и следующим индексом (когда текущий индекс — последний индекс, то следующий индекс предполагается равным длине массива вдоль выбранного измерения). Таким образом, цикл по одному измерению будет реализовывать сокращение по указанным индексам.

Обработка невыровненных данных или типов данных цикла, не соответствующих типу данных входных и/или выходных данных, выполняется с помощью буферизованного кода, в котором данные копируются во временной буфер и приводятся к нужному типу данных, если это необходимо, перед вызовом основной функции одного измерения. Временные буферы создаются размером (элементов) не больше заданного пользователем значения размера буфера. Таким образом, цикл должен быть достаточно гибким, чтобы вызывать основной вычислительный цикл по одному измерению достаточно раз, чтобы завершить все вычисления частями, не превышающими размер буфера.

© 2005–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.18/reference/internals.code-explanations.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API