В информатике префиксная сумма , кумулятивная сумма , инклюзивное сканирование или просто сканирование последовательности чисел x 0 , x 1 , x 2 , ... — это вторая последовательность чисел y 0 , y 1 , y 2 , ... , суммы префиксов ( текущие итоги ) входной последовательности:
Например, префиксные суммы натуральных чисел являются треугольными числами :
Префиксные суммы тривиально вычисляются в последовательных моделях вычислений, используя формулу y i = y i − 1 + x i для вычисления каждого выходного значения в порядке последовательности. Однако, несмотря на простоту вычисления, префиксные суммы являются полезным примитивом в некоторых алгоритмах, таких как сортировка подсчетом , [1] [2], и они формируют основу функции сканирования высшего порядка в функциональных языках программирования. Префиксные суммы также много изучались в параллельных алгоритмах , как в качестве тестовой задачи, которую нужно решить, так и в качестве полезного примитива для использования в качестве подпрограммы в других параллельных алгоритмах. [3] [4] [5]
Абстрактно, префиксная сумма требует только бинарного ассоциативного оператора ⊕ , что делает ее полезной для многих приложений, от вычисления хорошо разделенных парных разложений точек до обработки строк. [6] [7]
Математически операция взятия префиксных сумм может быть обобщена с конечных на бесконечные последовательности; в этом контексте префиксная сумма известна как частичная сумма ряда . Префиксное суммирование или частичное суммирование образуют линейные операторы на векторных пространствах конечных или бесконечных последовательностей; их обратные операторы являются операторами конечной разности .
В терминах функционального программирования префиксная сумма может быть обобщена на любую бинарную операцию (не только на операцию сложения ); функция более высокого порядка, полученная в результате этого обобщения, называется сканированием , и она тесно связана с операцией сложения . И сканирование, и операция сложения применяют заданную бинарную операцию к одной и той же последовательности значений, но отличаются тем, что сканирование возвращает всю последовательность результатов бинарной операции, тогда как сложение возвращает только конечный результат. Например, последовательность факториальных чисел может быть сгенерирована сканированием натуральных чисел с использованием умножения вместо сложения:
Реализации сканирования на языке программирования и в библиотеках могут быть как инклюзивными , так и исключительными . Инклюзивное сканирование включает входные данные x i при вычислении выходных данных y i (т. е. ), а исключающее сканирование — нет (т. е. ). В последнем случае реализации либо оставляют y 0 неопределенным, либо принимают отдельное значение " x −1 ", которым задается сканирование. Любой тип сканирования может быть преобразован в другой: инклюзивное сканирование может быть преобразовано в исключительное сканирование путем сдвига массива, полученного при сканировании, вправо на один элемент и вставки значения идентичности слева от массива. И наоборот, исключающее сканирование может быть преобразовано в инклюзивное сканирование путем сдвига массива, полученного при сканировании, влево и вставки суммы последнего элемента сканирования и последнего элемента входного массива справа от массива. [8]
В следующей таблице приведены примеры функций инклюзивного и эксклюзивного сканирования, предоставляемых несколькими языками программирования и библиотеками:
Модель параллельного программирования OpenMP на основе директив поддерживает как инклюзивное, так и эксклюзивное сканирование, начиная с версии 5.0.
Существует два ключевых алгоритма для вычисления суммы префикса параллельно. Первый предлагает более короткий промежуток и больший параллелизм , но неэффективен в плане работы. Второй эффективен в плане работы, но требует удвоенного промежутка и предлагает меньший параллелизм. Они представлены по очереди ниже.
Хилис и Стил представляют следующий алгоритм параллельной префиксной суммы: [9]
для i <- 0 до пола (log 2 ( n )) выполнить для j <- 0 до n - 1 выполнить параллельно если j < 2 i тогда xя +1
дж<- хя
дж еще хя +1
дж<- хя
дж+ хя
дж - 2 я
В приведенном выше обозначении обозначается значение j -го элемента массива x на временном шаге i .
С одним процессором этот алгоритм будет работать за время O ( n log n ) . Однако, если машина имеет по крайней мере n процессоров для параллельного выполнения внутреннего цикла, алгоритм в целом будет работать за время O (log n ) , количество итераций внешнего цикла.
Эффективную по работе параллельную префиксную сумму можно вычислить с помощью следующих шагов. [3] [10] [11]
Если входная последовательность имеет n шагов, то рекурсия продолжается до глубины O (log n ) , что также является границей параллельного времени выполнения этого алгоритма. Количество шагов алгоритма равно O ( n ) , и он может быть реализован на параллельной машине с произвольным доступом с O ( n /log n ) процессорами без какого-либо асимптотического замедления путем назначения нескольких индексов каждому процессору в раундах алгоритма, для которых элементов больше, чем процессоров. [3]
Каждый из предыдущих алгоритмов выполняется за время O (log n ) . Однако первый алгоритм занимает ровно log 2 n шагов, тогда как последний требует 2 log 2 n − 2 шагов. Для проиллюстрированных примеров с 16 входами алгоритм 1 является 12-сторонним параллельным (49 единиц работы, разделенных на промежуток 4), тогда как алгоритм 2 является только 4-сторонним параллельным (26 единиц работы, разделенных на промежуток 6). Однако алгоритм 2 эффективен по работе — он выполняет только постоянный множитель (2) от объема работы, требуемого последовательным алгоритмом — в то время как алгоритм 1 неэффективен по работе — он выполняет асимптотически больше работы (логарифмический множитель), чем требуется последовательно. Следовательно, алгоритм 1, вероятно, будет работать лучше, когда доступен обильный параллелизм, но алгоритм 2, вероятно, будет работать лучше, когда параллелизм более ограничен.
Параллельные алгоритмы для префиксных сумм часто можно обобщить для других операций сканирования на ассоциативных бинарных операциях , [3] [4] и их также можно эффективно вычислять на современном параллельном оборудовании, таком как графический процессор . [12] Идея создания на аппаратном уровне функционального блока, предназначенного для вычисления многопараметрической префиксной суммы, была запатентована Узи Вишкиным . [13]
Многие параллельные реализации следуют двухпроходной процедуре, где частичные суммы префиксов вычисляются в первом проходе на каждом процессорном блоке; префиксная сумма этих частичных сумм затем вычисляется и транслируется обратно в процессорные блоки для второго прохода, используя теперь известный префикс в качестве начального значения. Асимптотически этот метод занимает приблизительно две операции чтения и одну операцию записи на элемент.
Реализация параллельного алгоритма суммы префиксов, как и других параллельных алгоритмов, должна учитывать архитектуру параллелизации платформы. В частности, существует множество алгоритмов, адаптированных для платформ, работающих на общей памяти , а также алгоритмов, которые хорошо подходят для платформ, использующих распределенную память , полагаясь на передачу сообщений как на единственную форму межпроцессного взаимодействия.
Следующий алгоритм предполагает модель машины с общей памятью ; все элементы обработки (PE) имеют доступ к одной и той же памяти. Версия этого алгоритма реализована в Multi-Core Standard Template Library (MCSTL), [14] [15] параллельной реализации стандартной библиотеки шаблонов C++ , которая предоставляет адаптированные версии для параллельных вычислений различных алгоритмов.
Для того чтобы одновременно вычислить сумму префикса по элементам данных с элементами обработки, данные делятся на блоки, каждый из которых содержит элементы (для простоты мы предполагаем, что делит ). Обратите внимание, что хотя алгоритм делит данные на блоки, только элементы обработки работают параллельно в один момент времени.
В первом цикле каждый PE вычисляет локальную сумму префикса для своего блока. Последний блок не нужно вычислять, поскольку эти суммы префиксов вычисляются только как смещения к суммам префиксов последующих блоков, а последний блок по определению не является успешным.
Смещения , которые хранятся в последней позиции каждого блока, накапливаются в префиксной сумме их собственных и сохраняются в их последующих позициях. Поскольку это небольшое число, быстрее сделать это последовательно, для большого этот шаг можно сделать и параллельно.
Выполняется вторая развертка. На этот раз первый блок не нужно обрабатывать, так как ему не нужно учитывать смещение предыдущего блока. Однако в этой развертке вместо этого включается последний блок, и суммы префиксов для каждого блока вычисляются с учетом смещений блоков сумм префиксов, вычисленных в предыдущей развертке.
function prefix_sum ( elements ) { n := size ( elements ) p := количество обрабатываемых элементов prefix_sum : = [ 0. . .0 ] размера n do parallel i = 0 to p - 1 { // i : = индекс текущего PE от j = i * n / ( p + 1 ) до ( i + 1 ) * n / ( p + 1 ) - 1 do { // Это сохраняет только префиксную сумму локальных блоков store_prefix_sum_with_offset_in ( elements , 0 , prefix_sum ) } } x = 0 для i = 1 to p { // Последовательное накопление общей суммы блоков x += prefix_sum [ i * n / ( p + 1 ) - 1 ] // Построение префиксной суммы по первым p блокам prefix_sum [ i * n / ( p + 1 )] = x // Сохранение результатов для использования в качестве смещений во втором цикле } do parallel i = 1 to p { // i := индекс текущего PE от j = i * n / ( p + 1 ) до ( i + 1 ) * n / ( p + 1 ) - 1 do { offset := prefix_sum [ i * n / ( p + 1 )] // Вычислить сумму префикса, взяв сумму предыдущих блоков в качестве смещения store_prefix_sum_with_offset_in ( элементы , смещение , prefix_sum ) } } return prefix_sum }
Улучшение: в случае, если количество блоков слишком велико и последовательный шаг становится трудоемким из-за развертывания одного процессора, можно использовать алгоритм Хиллиса и Стила для ускорения второго этапа.
Алгоритм суммы префиксов гиперкуба [16] хорошо адаптирован для платформ с распределенной памятью и работает с обменом сообщениями между процессорными элементами. Он предполагает, что в алгоритме участвует процессорных элементов (PE), количество которых равно количеству углов в -мерном гиперкубе .
На протяжении всего алгоритма каждый PE рассматривается как угол в гипотетическом гиперкубе со знанием общей суммы префиксов , а также суммы префиксов всех элементов до него (в соответствии с упорядоченными индексами среди PE), оба в своем собственном гиперкубе.
В -мерном гиперкубе с PE в углах алгоритм должен быть повторен несколько раз, чтобы объединить нульмерные гиперкубы в одномерный -мерный гиперкуб. Предполагая дуплексную модель связи , где два соседних PE в разных гиперкубах могут обмениваться в обоих направлениях за один шаг связи, это означает запуски связи.
i : = Индекс собственного процессорного элемента ( PE ) m : = префиксная сумма локальных элементов этого PE d : = число измерений гиперкуба x = m ; // Инвариант: префиксная сумма до этого PE в текущем подкубе σ = m ; // Инвариант: префиксная сумма всех элементов в текущем подкубе for ( k = 0 ; k <= d - 1 ; k ++ ) { y = σ @ PE ( i xor 2 ^ k ) // Получить общую сумму префиксов противоположного подкуба по измерению k σ = σ + y // Агрегировать сумму префиксов обоих подкубов if ( i & 2 ^ k ) { x = x + y // Агрегировать только сумму префикса из другого подкуба, если этот PE имеет более высокий индекс. } }
Конвейерный алгоритм двоичного дерева [17] — еще один алгоритм для платформ с распределенной памятью, который особенно хорошо подходит для сообщений большого размера.
Как и алгоритм гиперкуба, он предполагает особую структуру связи. Элементы обработки (PE) гипотетически расположены в бинарном дереве (например, дереве Фибоначчи) с инфиксной нумерацией в соответствии с их индексом внутри PE. Связь на таком дереве всегда происходит между родительскими и дочерними узлами.
Инфиксная нумерация гарантирует, что для любого заданного PE j индексы всех узлов, достижимых его левым поддеревом, меньше , а индексы всех узлов в правом поддереве больше . Индекс родителя больше любого из индексов в поддереве PE j , если PE j является левым потомком, и меньше, если PE j является правым потомком. Это позволяет сделать следующие рассуждения:
Обратите внимание на различие между локальными для поддерева и общими префиксными суммами. Пункты два, три и четыре могут привести к мысли, что они образуют циклическую зависимость, но это не так. PE более низкого уровня могут потребовать общую сумму префикса PE более высокого уровня для расчета общей суммы префикса, но PE более высокого уровня требуют только локальные суммы префикса поддерева для расчета общей суммы префикса. Корневой узел как узел самого высокого уровня требует только локальную сумму префикса своего левого поддерева для расчета собственной суммы префикса. Каждый PE на пути от PE 0 до корня PE требует только локальную сумму префикса своего левого поддерева для расчета собственной суммы префикса, тогда как каждый узел на пути от PE p-1 (последний PE) до корня PE требует общую сумму префикса своего родителя для расчета собственной суммы префикса.
Это приводит к двухфазному алгоритму:
Восходящая фаза.
Распространение суммы локального префикса поддерева на его родительский элемент для каждого PE j .
Нисходящая фаза
Распространение исключительной (исключительный PE j , а также PE в его левом поддереве) общей суммы префиксов всех PE нижнего индекса, которые не включены в адресуемое поддерево PE j, на PE нижнего уровня в левом дочернем поддереве PE j . Распространение инклюзивной суммы префиксов на правое дочернее поддерево PE j .
Обратите внимание, что алгоритм выполняется параллельно на каждом PE, и PE будут блокироваться при получении до тех пор, пока их дочерние/родительские узлы не предоставят им пакеты.
k := количество пакетов в сообщении m PE m @ { left , right , parent , this } : = // Сообщения в разных PE х = м @ это // Восходящая фаза - вычисление локальных префиксных сумм поддерева для j = 0 до k - 1 : // Конвейеризация: для каждого пакета сообщения, если hasLeftChild : блокировка получения m [ j ] @ left // Это заменяет локальный m[j] на полученный m[j] // Агрегируем инклюзивную локальную префиксную сумму из PE с более низким индексом x [ j ] = m [ j ] ⨁ x [ j ] если hasRightChild : блокировка получения m [ j ] @ right // Мы не агрегируем m[j] в локальную сумму префиксов, так как правые потомки являются PE с более высоким индексом отправить x [ j ] ⨁ m [ j ] родителю else : отправить x [ j ] родителю // Нисходящая фаза для j = 0 до k - 1 : m [ j ] @ this = 0 если hasParent : блокировка получения m [ j ] @ parent // Для левого потомка m[j] — это исключительная префиксная сумма родителей, для правого потомка — включающая префиксная сумма x [ j ] = m [ j ] ⨁ x [ j ] отправить m [ j ] налево // Общая префиксная сумма всех PE, меньших этого или любого PE в левом поддереве отправить x [ j ] направо // Общая префиксная сумма всех PE, меньших или равных этому PE
Если сообщение m длины n можно разделить на k пакетов и оператор ⨁ можно использовать для каждого соответствующего пакета сообщения отдельно, то возможна конвейеризация . [17]
Если алгоритм используется без конвейеризации, всегда есть только два уровня (отправляющие PE и принимающие PE) двоичного дерева в работе, пока все остальные PE ждут. Если есть p элементов обработки и используется сбалансированное двоичное дерево, дерево имеет уровни, длина пути от до составляет , поэтому что представляет собой максимальное количество непараллельных операций связи во время восходящей фазы, аналогично, связь на нисходящем пути также ограничена запусками. Предполагая, что время запуска связи и время побайтовой передачи , восходящая и нисходящая фазы ограничены в неконвейерном сценарии.
При разделении на k пакетов, каждый размером и отправке их отдельно, первый пакет все еще должен быть распространен как часть локальной суммы префиксов, и это произойдет снова для последнего пакета, если . Однако между ними все PE на пути могут работать параллельно, и каждая третья операция связи (получить левый, получить правый, отправить родительскому элементу) отправляет пакет на следующий уровень, так что одна фаза может быть завершена в операциях связи, и обе фазы вместе требуют , что благоприятно для больших размеров сообщений n .
Алгоритм можно дополнительно оптимизировать, используя полнодуплексную или телефонную модель связи и накладывая восходящую и нисходящую фазы. [17]
Когда набор данных может обновляться динамически, он может храниться в структуре данных дерева Фенвика . Эта структура позволяет как искать любое индивидуальное значение суммы префикса, так и изменять любое значение массива за логарифмическое время на операцию. [18] Однако в более ранней статье 1982 года [19] представлена структура данных, называемая деревом частичных сумм (см. раздел 5.1), которая, по-видимому, перекрывает деревья Фенвика; в 1982 году термин «префиксная сумма» еще не был так распространен, как сегодня.
Для массивов более высокой размерности таблица суммированной площади предоставляет структуру данных, основанную на префиксных суммах для вычисления сумм произвольных прямоугольных подмассивов. Это может быть полезным примитивом в операциях свертки изображений . [20]
Сортировка подсчетом — это алгоритм сортировки целых чисел , который использует префиксную сумму гистограммы частот ключей для вычисления позиции каждого ключа в отсортированном выходном массиве. Он работает за линейное время для целочисленных ключей, которые меньше количества элементов, и часто используется как часть сортировки по основанию , быстрого алгоритма сортировки целых чисел, которые менее ограничены по величине. [1]
Ранжирование списка , проблема преобразования связанного списка в массив , представляющий ту же последовательность элементов, может рассматриваться как вычисление префиксной суммы последовательности 1, 1, 1, ... и последующее сопоставление каждого элемента с позицией массива, заданной его значением префиксной суммы; путем объединения ранжирования списка, префиксных сумм и циклов Эйлера многие важные проблемы на деревьях могут быть решены с помощью эффективных параллельных алгоритмов. [4]
Раннее применение алгоритмов параллельной префиксной суммы было в разработке двоичных сумматоров , булевых схем, которые могут складывать два n -битных двоичных числа. В этом приложении последовательность битов переноса сложения может быть представлена как операция сканирования последовательности пар входных битов, используя функцию большинства для объединения предыдущего переноса с этими двумя битами. Затем каждый бит выходного числа может быть найден как исключающее или двух входных битов с соответствующим битом переноса. Используя схему, которая выполняет операции алгоритма параллельной префиксной суммы, можно разработать сумматор, который использует O ( n ) логических вентилей и O (log n ) временных шагов. [3] [10] [11]
В модели вычислений с параллельным произвольным доступом префиксные суммы могут использоваться для моделирования параллельных алгоритмов, которые предполагают возможность для нескольких процессоров получать доступ к одной и той же ячейке памяти одновременно на параллельных машинах, которые запрещают одновременный доступ. С помощью сортировочной сети набор параллельных запросов на доступ к памяти может быть упорядочен в последовательность таким образом, что доступы к одной и той же ячейке будут смежными в последовательности; затем операции сканирования могут использоваться для определения того, какой из доступов успешно записал запрошенные ячейки, и для распределения результатов операций чтения памяти между несколькими процессорами, которые запрашивают тот же результат. [21]
В докторской диссертации Гая Блеллоха [22] параллельные префиксные операции являются частью формализации модели параллелизма данных , предоставляемой такими машинами, как Connection Machine . Connection Machine CM-1 и CM-2 предоставляли гиперкубическую сеть, на которой мог быть реализован Алгоритм 1 выше, тогда как CM-5 предоставлял выделенную сеть для реализации Алгоритма 2. [23]
При построении кодов Грея , последовательностей двоичных значений со свойством, что последовательные значения последовательности отличаются друг от друга в одной битовой позиции, число n может быть преобразовано в значение кода Грея в позиции n последовательности, просто взяв исключающее ИЛИ n и n / 2 (число, образованное сдвигом n вправо на одну битовую позицию). Обратная операция, декодирование значения Грея x в двоичное число, более сложна, но может быть выражена как префиксная сумма битов x , где каждая операция суммирования в префиксной сумме выполняется по модулю два. Префиксная сумма этого типа может быть эффективно выполнена с использованием побитовых булевых операций, доступных на современных компьютерах, путем вычисления исключающего ИЛИ x с каждым из чисел, образованных сдвигом x влево на количество битов, которое является степенью двух. [ 24]
Параллельный префикс (использующий умножение в качестве базовой ассоциативной операции) также может использоваться для построения быстрых алгоритмов для параллельной полиномиальной интерполяции . В частности, его можно использовать для вычисления коэффициентов разделенной разности формы Ньютона интерполяционного полинома. [25] Этот подход на основе префикса также можно использовать для получения обобщенных разделенных разностей для (конфлюэнтной) интерполяции Эрмита , а также для параллельных алгоритмов для систем Вандермонда . [26]
Префиксная сумма используется для балансировки нагрузки как малозатратный алгоритм распределения работы между несколькими процессорами, где главной целью является достижение равного объема работы на каждом процессоре. Алгоритмы используют массив весов, представляющих объем работы, требуемый для каждого элемента. После вычисления префиксной суммы рабочий элемент i отправляется на обработку в процессорный блок с номером . [27] Графически это соответствует операции, в которой объем работы в каждом элементе представлен длиной линейного сегмента, все сегменты последовательно размещаются на линии, а результат разрезается на количество частей, соответствующее количеству процессоров. [28]