Объяснения кода NumPy на C
Фанатизм заключается в удвоении усилий, когда вы забыли о своей цели. — Джордж Сантаяна
Авторитет — это человек, который может рассказать вам больше о чём-то, чем вам действительно хочется знать. — Неизвестно
В этой главе мы пытаемся объяснить логику некоторых новых фрагментов кода. Цель этих объяснений — позволить кому-либо понять идеи, лежащие в основе реализации, несколько легче, чем просто рассматривать код. Возможно, таким образом алгоритмы можно будет улучшить, заимствовать и/или оптимизировать.
Модель памяти
Одним из фундаментальных аспектов ndarray является то, что массив рассматривается как «блок» памяти, начинающийся в определённом месте. Интерпретация этой памяти зависит от информации о шагах. Для каждой размерности в
-мерном массиве целое число (шаг) определяет, сколько байт необходимо пропустить, чтобы перейти к следующему элементу в этой размерности. Если у вас нет массива с одним сегментом, эта информация о шагах должна учитываться при прохождении по массиву. Несложно написать код, который принимает шаги, вам просто нужно использовать указатели (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-contiguous. Макрос работает, сначала сделав специальный случай для C-contiguous, 1-D и 2-D случаев, которые работают очень просто.
В общем случае итерация происходит путём отслеживания списка счётчиков координат в объекте итератора. На каждой итерации последний счётчик координат увеличивается (начиная с 0). Если этот счётчик меньше, чем значение, на единицу меньшее, чем размер массива в этой размерности (предварительно вычисленное и сохранённое значение), то счётчик увеличивается, и член dataptr увеличивается на шаги в этой размерности, и макрос завершается. Если достигается конец размерности, счётчик последней размерности сбрасывается до нуля, а dataptr возвращается к началу этой размерности путём вычитания значения шагов, умноженного на значение, на единицу меньшее, чем количество элементов в этой размерности (это также предварительно вычислено и сохранено в члене backstrides объекта итератора). В этом случае макрос не завершается, но локальный счётчик размерности уменьшается, так что размерность перед последней принимает роль последней размерности, и описанные выше тесты выполняются снова для предпоследней размерности. Таким образом, dataptr корректируется должным образом для произвольных шагов.
Член coordinates структуры PyArrayIterObject поддерживает текущий N-мерный счётчик, если подлежащий массив не является C-contiguous, в этом случае подсчёт координат пропускается. Член index объекта PyArrayIterObject отслеживает текущий плоский индекс итератора. Он обновляется макросом PyArray_ITER_NEXT.
Вещание
В Numeric вещание реализовывалось в нескольких строках кода, глубоко запрятанных в 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
- срез
- многоточие
- массивы/массивоподобные объекты целых чисел (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 не запрошено выделить результат.
После подготовки получение и установка относительно просты, хотя необходимо учитывать различные режимы итерации. Если во время получения элемента есть только один массив индексов, проверка корректности индексов выполняется предварительно. В противном случае она обрабатывается в самом внутреннем цикле для оптимизации.
Универсальные функции
Универсальные функции — это вызываемые объекты, которые принимают
входные данные и производят
выходные данные, оборачивая базовые циклы 1-D, работающие поэлементно, в полные удобные функции, которые бесшовным образом реализуют трансляцию, проверку типов и буферизованное приведение типов и обработку выходных аргументов. Новые универсальные функции обычно создаются на C, хотя существует механизм создания универсальных функций из функций Python (frompyfunc). Пользователь должен предоставить цикл 1-D, который реализует основную функцию, принимающую скалярные входные значения и помещающую полученные скаляры в соответствующие выходные слоты, как описано в реализации.
Настройка
Каждый расчет универсальной функции включает в себя некоторую издержки, связанные с настройкой расчета. Практическое значение этих издержек заключается в том, что, хотя фактический расчет универсальной функции очень быстрый, вы можете написать код, специфичный для массива и типа, который будет работать быстрее для небольших массивов, чем универсальная функция. В частности, использование универсальных функций для выполнения многих вычислений над массивами 0-D будет медленнее, чем другие решения на основе Python (молча импортированный модуль scalarmath существует именно для того, чтобы массивы скаляров имели внешний вид вычислений на основе универсальных функций с значительно меньшими накладными расходами).
При вызове универсальной функции необходимо выполнить множество действий. Информация, собранная из этих операций настройки, хранится в объекте цикла. Этот объект цикла — структура C (которая может стать объектом Python, но не инициализирована как таковая, потому что используется только во внутреннем контексте). Этот объект цикла имеет структуру, необходимую для использования с PyArray_Broadcast, чтобы трансляция обрабатывалась так же, как и в других частях кода.
Первым действием является поиск в глобальном словаре, специфичном для потока, текущих значений для размера буфера, маски ошибок и связанного объекта ошибки. Состояние маски ошибок контролирует, что происходит при обнаружении условия ошибки. Следует отметить, что проверка флагов ошибок оборудования выполняется только после выполнения каждого цикла 1-D. Это означает, что если входные и выходные массивы непрерывны и имеют правильный тип, так что выполняется один цикл 1-D, то флаги могут не проверяться до тех пор, пока не будут вычислены все элементы массива. Поиск этих значений в словаре, специфичном для потока, занимает время, которое легко игнорировать для всех, кроме очень маленьких массивов.
После проверки потоковых глобальных переменных входные данные оцениваются, чтобы определить, как должна продолжить работу универсальная функция, и входные и выходные массивы создаются при необходимости. Любые входные данные, которые не являются массивами, преобразуются в массивы (при необходимости с использованием контекста). Отмечается, какие из входных данных являются скалярами (и, следовательно, преобразуются в массивы 0-D).
Далее выбирается подходящий цикл 1-D из имеющихся у универсальной функции на основе типов входных массивов. Этот цикл 1-D выбирается путем попытки сопоставить сигнатуру типов данных входных данных с доступными сигнатурами. Сигнатуры, соответствующие встроенным типам, хранятся в члене types структуры универсальной функции. Сигнатуры, соответствующие типам, определенным пользователем, хранятся в связанном списке информации о функциях, где головной элемент хранится как CObject в словаре userloops с ключом в виде номера типа данных (используется первый тип, определенный пользователем, в списке аргументов, в качестве ключа). Сигнатуры просматриваются до тех пор, пока не будет найдена сигнатура, в которую все входные массивы могут быть безопасно преобразованы (игнорируя любые скалярные аргументы, которые не могут определить тип результата). Следствием этой процедуры поиска является то, что «меньшие типы» должны быть размещены ниже «больших типов» при хранении сигнатур. Если цикл 1-D не найден, сообщается об ошибке. В противном случае список аргументов обновляется сохраненной сигнатурой — в случае необходимости преобразования и для исправления выходных типов, предполагаемых циклом 1-D.
Если универсальная функция имеет 2 входных и 1 выходной данные и второй входной массив является массивом объектов, выполняется проверка специального случая, чтобы вернуть NotImplemented, если второй входной массив не является ndarray, не имеет атрибут __array_priority__, и не имеет специальный метод __r{op}__. Таким образом, Python получает сигнал, чтобы дать другому объекту шанс выполнить операцию вместо использования универсальных вычислений над массивами объектов. Это позволяет (например) разреженным матрицам переопределять оператор умножения циклом 1-D.
Для входных массивов, меньших заданного размера буфера, создаются копии всех несмежных, невыровненных или несовпадающих по порядку байтов массивов, чтобы гарантировать, что для малых массивов используется один цикл. Затем создаются итераторы массивов для всех входных массивов, и полученный набор итераторов транслируется в один размер.
Затем обрабатываются выходные аргументы (если они есть), и создаются любые отсутствующие выходные массивы. Если какой-либо предоставленный выходной массив не имеет правильного типа (или невыровнен) и меньше размера буфера, то создается новый выходной массив со флагом UPDATEIFCOPY, чтобы при DECREF после завершения функции его содержимое было скопировано обратно в выходной массив. Затем обрабатываются итераторы для выходных аргументов.
Наконец, принимается решение о том, как выполнить механизм цикла, чтобы убедиться, что все элементы входных массивов комбинируются для получения выходных массивов правильного типа. Варианты выполнения цикла: один цикл (для непрерывных, выровненных и правильного типа данных), цикл с шагом (для несмежных, но все еще выровненных и правильного типа данных) и буферизованный цикл (для невыровненных или неправильного типа данных). В зависимости от вызываемого метода выполнения, цикл затем настраивается и вычисляется.
Вызов функции
В этом разделе описывается, как настраивается и выполняется основной цикл вычисления универсальной функции для каждого из трех различных типов выполнения. Если NPY_ALLOW_THREADS определено во время компиляции, то, до тех пор пока не будут задействованы массивы объектов, блокировка глобального интерпретатора Python (GIL) освобождается перед вызовом циклов. При необходимости она восстанавливается для обработки условий ошибок. Флаги ошибок оборудования проверяются только после завершения цикла 1-D.
Один цикл
Это самый простой случай. Универсальная функция выполняется путем вызова базового цикла 1-D ровно один раз. Это возможно только тогда, когда у нас есть выровненные данные правильного типа (включая порядок байтов) для входных и выходных данных, и все массивы имеют одинаковые шаги (или непрерывные, 0-D или 1-D). В этом случае цикл вычислений 1-D вызывается один раз для вычисления расчета для всего массива. Обратите внимание, что флаги ошибок оборудования проверяются только после завершения всего вычисления.
Цикл с шагом
Когда входные и выходные массивы выровнены и имеют правильный тип, но шаги не одинаковы (несмежные и 2-D или более), для расчета используется вторая структура цикла. Этот подход преобразует все итераторы входных и выходных аргументов в итерацию по всем, кроме самого большого измерения. Затем внутренний цикл обрабатывается базовым циклом вычислений 1-D. Внешний цикл — это стандартный цикл итерации по преобразованным итераторам. Флаги ошибок оборудования проверяются после завершения каждого цикла 1-D.
Буферизованный цикл
В этом коде обрабатываются ситуации, когда входные и/или выходные массивы не соответствуют ожидаемому выравниванию или типу данных (включая перестановку байтов) в базовом 1-мерном цикле. Также предполагается, что массивы несмежные. Код очень похож на код с шагом-циклом, за исключением того, что внутренний 1-мерный цикл модифицирован таким образом, что предварительная обработка выполняется над входными данными, а последующая обработка — над выходными данными в блоках размером bufsize (где bufsize — параметр, задаваемый пользователем). Базовый 1-мерный вычислительный цикл вызывается с данными, которые копируются (если это необходимо). Код настройки и цикла значительно сложнее в этом случае, поскольку он должен обрабатывать:
- выделение памяти для временных буферов
- определение, использовать ли буферы для входных и выходных данных (неправильное выравнивание и/или тип данных)
- копирование и, возможно, приведение типов данных для любых входных или выходных данных, для которых необходимы буферы.
- специальное обращение с массивами типа Object, чтобы правильно обрабатывать счётчики ссылок при копировании и/или приведении типов.
- разбиение внутреннего 1-мерного цикла на блоки размером bufsize (с возможным остатком).
Опять же, флаги ошибок оборудования проверяются в конце каждого 1-мерного цикла.
Конечная обработка выходных данных
Ufuncs позволяют другим похожим на массив классам беспрепятственно передаваться через интерфейс, так как входные данные определённого класса приведут к тому, что выходные данные будут того же класса. Механизм работы следующий. Если какой-либо из входов не является nd-массивом и определяет метод __array_wrap__, то класс с наибольшим атрибутом __array_priority__ определяет тип всех выходных данных (за исключением любых выходных массивов, которые передаются). Метод __array_wrap__ входного массива будет вызван с nd-массивом, возвращаемым ufunc, как входом. Поддерживаются два стиля вызова функции __array_wrap__. Первый принимает nd-массив в качестве первого аргумента и кортеж «контекста» в качестве второго. Контекст — это (ufunc, аргументы, номер выходного аргумента). Это первый вызов, который будет предпринят. Если возникает ошибка TypeError, то функция вызывается только с nd-массивом в качестве первого аргумента.
Методы
Существует три метода ufuncs, требующих вычислений, аналогичных универсальным ufuncs. Это reduce, accumulate и reduceat. Каждый из этих методов требует команды настройки, за которой следует цикл. Возможны четыре стиля циклов для этих методов: без элементов, с одним элементом, с шагом и с буфером. Это те же самые основные стили циклов, которые реализованы для вызовов универсальных функций, за исключением случаев без элементов и с одним элементом, которые являются особыми случаями, возникающими, когда объекты входных массивов имеют 0 и 1 элемент соответственно.
Настройка
Функция настройки для всех трёх методов — construct_reduce. Эта функция создаёт объект цикла reduce и заполняет его параметрами, необходимыми для завершения цикла. Все методы работают только с ufuncs, принимающими 2 входных значения и возвращающими 1 выходное значение. Следовательно, базовый 1-мерный цикл выбирается с сигнатурой [ otype, otype, otype ], где otype — запрашиваемый тип данных для операции reduce. Размер буфера и обработка ошибок затем извлекаются из глобального хранилища (по потоку). Для малых массивов, которые не выровнены или имеют неправильный тип данных, делается копия, чтобы использовать небуферизованный фрагмент кода. Затем выбирается стратегия цикла. Если в массиве 1 или 0 элементов, то выбирается простой метод цикла. Если массив выровнен и имеет правильный тип данных, то выбирается цикл с шагом. В противном случае необходимо выполнить буферизованный цикл. Параметры цикла устанавливаются, и строится возвращаемый массив. Форма выходного массива отличается в зависимости от того, является ли метод reduce, accumulate или reduceat. Если выходной массив уже предоставлен, проверяется его форма. Если выходной массив не является C-непрерывным, невыровненным и не имеет правильного типа данных, то создаётся временная копия с установленным флагом UPDATEIFCOPY. Таким образом, методы смогут работать с хорошо организованным выходным массивом, но результат будет скопирован обратно в настоящий выходной массив, когда вычисления метода будут завершены. Наконец, инициализируются итераторы для цикла по нужному осевому направлению (в зависимости от значения axis, переданного методу), и процедура настройки возвращается к фактической вычислительной процедуре.
Reduce
Все методы ufunc используют одни и те же базовые 1-мерные вычислительные циклы с входными и выходными аргументами, скорректированными таким образом, чтобы выполнялось соответствующее сокращение. Например, ключ к работе reduce заключается в том, что 1-мерный цикл вызывается с выходом и вторым входом, указывающими на одну и ту же позицию в памяти, и оба имеют шаг 0. Первый вход указывает на входной массив с шагом, задаваемым соответствующим шагом для выбранной оси. Таким образом, выполняемая операция —
![\begin{align*}
o & = & i[0] \\
o & = & i[k]\textrm{<op>}o\quad k=1\ldots N
\end{align*}](https://docs.scipy.org/doc/numpy-1.13.0/_images/math/e84ac41da48293683e7736944c7b697b131a4dca.png)
где
— количество элементов во входном массиве,
,
— выходной массив, а
—
элемент
по выбранной оси. Эта базовая операция повторяется для массивов с более чем одним измерением, чтобы операция сокращения выполнялась для каждого 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*}](https://docs.scipy.org/doc/numpy-1.13.0/_images/math/e1486dfed0186fae30752b33ae67b1cf7c705419.png)
Выходной массив имеет ту же форму, что и входной, и каждый 1-мерный цикл работает над
элементами, когда форма по выбранной оси составляет
. Опять же, буферизованные циклы заботятся о копировании и приведении данных перед вызовом базового 1-мерного вычислительного цикла.
Reduceat
Функция reduceat — обобщение функций reduce и accumulate. Она реализует операцию reduce над диапазонами входного массива, заданными индексами. Дополнительный аргумент индексов проверяется, чтобы убедиться, что каждый вход не слишком велик для входного массива по выбранному измерению, прежде чем выполняются вычисления цикла. Реализация цикла обрабатывается кодом, очень похожим на код reduce, повторяемым столько раз, сколько элементов в входных индексах. В частности: указатель первого входа, переданный базовому 1-мерному вычислительному циклу, указывает на входной массив в правильном месте, указанном массивом индексов. Кроме того, указатель выхода и второй указатель входа, переданные базовому 1-мерному циклу, указывают на одну и ту же позицию в памяти. Размер 1-мерного вычислительного цикла фиксируется как разность между текущим индексом и следующим индексом (если текущий индекс — последний индекс, то следующий индекс предполагается равным длине массива по выбранному измерению). Таким образом, 1-мерный цикл будет реализовывать reduce по указанным индексам.
Неправильное выравнивание или тип данных цикла, не соответствующий типу данных входных и/или выходных данных, обрабатывается с помощью буферизованного кода, где данные копируются во временной буфер и приводятся к нужному типу данных при необходимости перед вызовом базовой 1-мерной функции. Временные буферы создаются размером (элемент) не больше значения, задаваемого пользователем, для размера буфера. Таким образом, цикл должен быть достаточно гибким, чтобы вызывать базовый 1-мерный вычислительный цикл достаточно раз для завершения всех вычислений в блоках размером не больше размера буфера.
© 2008–2017 NumPy Developers
Licensed under the NumPy License.
https://docs.scipy.org/doc/numpy-1.13.0/reference/internals.code-explanations.html