Параллелизм данных — это распараллеливание между несколькими процессорами в параллельных вычислительных средах. Он фокусируется на распределении данных по разным узлам, которые работают с данными параллельно. Его можно применять к обычным структурам данных, таким как массивы и матрицы, работая над каждым элементом параллельно. Он контрастирует с параллелизмом задач как с другой формой параллелизма.
Параллельное задание по данным на массиве из n элементов можно разделить поровну между всеми процессорами. Предположим, что мы хотим суммировать все элементы заданного массива, а время для одной операции сложения составляет Ta единиц времени. В случае последовательного выполнения время, затраченное процессом, составит n × Ta единиц времени, поскольку он суммирует все элементы массива. С другой стороны, если мы выполним это задание как параллельное задание по данным на 4 процессорах, затраченное время сократится до ( n /4) × Ta + единицы времени накладных расходов на слияние. Параллельное выполнение приводит к ускорению в 4 раза по сравнению с последовательным выполнением. Важно отметить, что локальность ссылок на данные играет важную роль в оценке производительности модели параллельного программирования данных. Локальность данных зависит от доступа к памяти, выполняемого программой, а также от размера кэша.
Эксплуатация концепции параллелизма данных началась в 1960-х годах с разработкой машины Соломона. [1] Машина Соломона, также называемая векторным процессором , была разработана для ускорения выполнения математических операций путем работы с большим массивом данных (работая с несколькими данными в последовательных временных шагах). Параллелизм операций с данными также использовался путем работы с несколькими данными одновременно с использованием одной инструкции. Эти процессоры назывались «процессорами массивов». [2] В 1980-х годах этот термин был введен [3] для описания этого стиля программирования, который широко использовался для программирования машин соединений на языках параллельной обработки данных, таких как C* . Сегодня параллелизм данных лучше всего иллюстрируется графическими процессорами (GPU), которые используют обе техники работы с несколькими данными в пространстве и времени с использованием одной инструкции.
Большинство аппаратных средств параллельной обработки данных поддерживают только фиксированное количество параллельных уровней, часто только один. Это означает, что в рамках параллельной операции невозможно запустить больше параллельных операций рекурсивно, и означает, что программисты не могут использовать вложенный аппаратный параллелизм. Язык программирования NESL был ранней попыткой реализовать модель вложенного программирования параллельных данных на машинах с плоскими параллельными вычислительными единицами и, в частности, ввел преобразование выравнивания , которое преобразует вложенный параллелизм данных в плоский параллелизм данных. Эта работа была продолжена другими языками, такими как Data Parallel Haskell и Futhark , хотя произвольный вложенный параллелизм данных не так широко доступен в современных языках программирования параллельных данных.
В многопроцессорной системе, выполняющей один набор инструкций ( SIMD ), параллелизм данных достигается, когда каждый процессор выполняет одну и ту же задачу на разных распределенных данных. В некоторых ситуациях один поток выполнения управляет операциями на всех данных. В других случаях разные потоки управляют операцией, но они выполняют один и тот же код.
Например, рассмотрим последовательное умножение и сложение матриц , как обсуждалось в примере.
Ниже представлен последовательный псевдокод для умножения и сложения двух матриц, где результат сохраняется в матрице C. Псевдокод для умножения вычисляет скалярное произведение двух матриц A , B и сохраняет результат в выходной матрице C.
Если бы следующие программы выполнялись последовательно, время, необходимое для вычисления результата, составило бы (предполагая, что длины строк и столбцов обеих матриц равны n) и для умножения и сложения соответственно.
// Умножение матриц для ( i = 0 ; i < row_length_A ; i ++ ) { для ( k = 0 ; k < column_length_B ; k ++ ) { sum = 0 ; для ( j = 0 ; j < column_length_A ; j ++ ) { sum += A [ i ][ j ] * B [ j ][ k ]; } C [ i ][ k ] = sum ; } }
// Сложение массива для ( i = 0 ; i < n ; i ++ ) { c [ i ] = a [ i ] + b [ i ]; }
Мы можем использовать параллелизм данных в предыдущем коде, чтобы выполнить его быстрее, поскольку арифметика независима от цикла. Распараллеливание кода умножения матриц достигается с помощью OpenMP . Директива OpenMP "omp parallel for" предписывает компилятору выполнять код в цикле for параллельно. Для умножения мы можем разделить матрицы A и B на блоки по строкам и столбцам соответственно. Это позволяет нам вычислять каждый элемент в матрице C по отдельности, тем самым делая задачу параллельной. Например: A[mxn] dot B [nxk] может быть завершено за вместо того, чтобы при параллельном выполнении с использованием m*k процессоров.
// Параллельное умножение матриц #pragma omp parallel for schedule(dynamic,1) collapse(2) for ( i = 0 ; i < row_length_A ; i ++ ){ for ( k = 0 ; k < column_length_B ; k ++ ){ sum = 0 ; for ( j = 0 ; j < column_length_A ; j ++ ){ sum += A [ i ][ j ] * B [ j ][ k ]; } C [ i ][ k ] = sum ; } }
Из примера можно увидеть, что потребуется много процессоров, поскольку размеры матриц продолжают увеличиваться. Сохранение низкого времени выполнения является приоритетом, но по мере увеличения размера матрицы мы сталкиваемся с другими ограничениями, такими как сложность такой системы и связанные с ней затраты. Поэтому, ограничивая количество процессоров в системе, мы все равно можем применить тот же принцип и разделить данные на более крупные фрагменты для вычисления произведения двух матриц. [4]
Для сложения массивов в реализации параллельной обработки данных предположим более скромную систему с двумя центральными процессорами (ЦП) A и B, ЦП A может складывать все элементы из верхней половины массивов, в то время как ЦП B может складывать все элементы из нижней половины массивов. Поскольку два процессора работают параллельно, работа по сложению массивов займет половину времени выполнения той же операции последовательно с использованием только одного ЦП.
Программа, представленная в виде псевдокода ниже, которая применяет некоторую произвольную операцию foo
к каждому элементу массива, d
иллюстрирует параллелизм данных: [примечание 1]
если ЦП = "а" , то нижний_лимит := 1 верхний_предел := раунд(d.длина / 2)иначе если ЦП = "b" тогда нижний_предел := раунд(d.длина / 2) + 1 верхний_предел := d.длинадля i от нижнего_предела до верхнего_предела на 1 сделать фу(д[я])
В системе SPMD , работающей на двухпроцессорной системе, оба ЦП будут выполнять код.
Параллелизм данных подчеркивает распределенную (параллельную) природу данных, в отличие от обработки (параллелизм задач). Большинство реальных программ находятся где-то на континууме между параллелизмом задач и параллелизмом данных.
Процесс распараллеливания последовательной программы можно разбить на четыре отдельных шага. [5]
[6]
Параллелизм данных и задач может быть реализован одновременно путем их объединения для одного приложения. Это называется смешанным параллелизмом данных и задач. Смешанный параллелизм требует сложных алгоритмов планирования и программной поддержки. Это лучший вид параллелизма, когда связь медленная, а количество процессоров велико. [7]
Смешанный параллелизм данных и задач имеет множество приложений. Он особенно используется в следующих приложениях:
Сегодня доступны различные среды параллельного программирования данных, наиболее широко используемые из которых:
Параллелизм данных находит применение в различных областях, от физики, химии, биологии, материаловедения до обработки сигналов. Науки подразумевают параллелизм данных для моделирования таких моделей, как молекулярная динамика [9] , анализ последовательностей геномных данных [10] и других физических явлений. Движущими силами в обработке сигналов для параллелизма данных являются видеокодирование, обработка изображений и графики, беспроводная связь [11] и многие другие.
d.length
оценивается как 1 и round
округляется до нуля [это всего лишь пример, нет никаких требований к типу округления]) приведут к тому, lower_limit
что значение будет больше upper_limit
, предполагается, что цикл немедленно завершится (т.е. произойдет ноль итераций), когда это произойдет.