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

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

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

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

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

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

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

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

Смотрите также

Объекты типа данных (dtype)

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

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

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

Смотрите также

Итерация по массивам

Очень распространённой операцией во многих кодах NumPy является необходимость итерации по всем элементам общего, страйдового, N-мерного массива. Эта операция общего N-мерного цикла абстрагируется в понятии объекта-итератора. Чтобы написать N-мерный цикл, вам нужно только создать объект-итератор из ndarray, работать с dataptr член структуры объекта итератора и вызов макроса PyArray_ITER_NEXT на объекте итератора для перехода к следующему элементу. The next элемент всегда находится в порядке C-contiguous. Макрос работает, сначала обрабатывая особые случаи C-contiguous, 1-D и 2-D, которые работают очень просто.

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

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

Трансляция (Broadcasting)#

Смотрите также

Трансляция (Broadcasting)

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

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

Вещание всегда реализовывалось в Numeric с использованием шагов со значением 0 для расширенных измерений. Это делается точно так же в NumPy. Большая разница в том, что теперь массив шагов отслеживается в PyArrayIterObject, итераторы, участвующие в результате broadcast, отслеживаются в PyArrayMultiIterObject, и PyArray_Broadcast вызов реализует Общие правила трансляции (broadcasting).

Скаляры массива#

Смотрите также

Скаляры

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

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

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

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

  • целое число

  • newaxis

  • срез

  • Ellipsis

  • целочисленные массивы/массивоподобные объекты (продвинутый уровень)

  • логический (один логический массив); если есть более одного логического массива в качестве индекса или форма не совпадает точно, логический массив будет преобразован в целочисленный массив.

  • 0-мерный булев (а также целочисленный); 0-мерные булевы массивы — это особый случай, который должен обрабатываться в коде расширенной индексации. Они сигнализируют, что 0-мерный булев массив должен интерпретироваться как целочисленный массив.

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

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

Расширенная индексация#

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

  • Существует один массив индексов, и он, как и массив присваивания, может быть тривиально итерирован. Например, они могут быть непрерывными. Также массив индексов должен быть intp тип и массив значений в присваиваниях должны быть правильного типа. Это исключительно быстрый путь.

  • Существуют только целочисленные индексы массивов, поэтому подмассивов не существует.

  • Смешано индексирование на основе представления и расширенное индексирование. В этом случае индексирование на основе представления определяет набор подмассивов, которые объединяются расширенным индексированием. Например, arr[[1, 2, 3], :] создаётся путём вертикального объединения подмассивов arr[1, :], arr[2, :], и arr[3, :].

  • Существует подмассив, но он имеет ровно один элемент. Этот случай может быть обработан как если бы подмассива не было, но требует некоторой осторожности при настройке.

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

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

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

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

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

Настройка#

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

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

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

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

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

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

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

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

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

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

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

Один цикл#

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

Страйдовый цикл#

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

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

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

  • выделение памяти для временных буферов

  • решая, использовать ли буферы на входных и выходных данных (невыровненные и/или неправильного типа данных)

  • копирование и, возможно, приведение данных для любых входов или выходов, для которых необходимы буферы.

  • специальная обработка Object массивы так, чтобы счетчики ссылок корректно обрабатывались при необходимости копирования и/или приведения типов.

  • разбивая внутренний 1-D цикл на bufsize блоки (с возможным остатком).

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

Финальная обработка вывода#

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

Методы#

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

Настройка#

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

Reduce#

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

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

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

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

Accumulate#

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

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

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

Reduceat#

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

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