RefNUMA – библиотека для организации виртуальной общей памяти в программах, использующих MPI.

А. О. Лацис

Пример 6. Параллельный цикл. Упрощенная форма записи создания массива общей памяти.

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

Понятие массива тесно связано с понятием цикла. В самом деле, массив предназначен, в первую очередь, для того, чтобы что-то делать с его элементами в теле цикла. Блочно распределенный разделяемый массив RefNUMA в этом плане не исключение. Ему соответствует специальная форма цикла – параллельный цикл RefNUMA.

Понятие параллельного цикла встречается во многих системах параллельного программирования, и, как правило, сводится к следующему.

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

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

Дадим строгое определение параллельного цикла RefNUMA: «Для каждого значения переменной цикла i из заданного диапазона выполнить в данном процессе те витки параллельного цикла, для которых i-я строка блочно распределенного массива указанной длины (в строках) является в данном процессе своей». Указывать конкретный управляющий массив при этом не требуется, поскольку все массивы одной и той же длины распределяются по процессам одинаково, блоками из одного и того же количества строк. Попросту говоря, параллельный цикл RefNUMA служит для того, чтобы поручить каждому процессу обработку своей для этого процесса части распределенного массива.

Заголовок параллельного цикла в RefNUMA записывается в виде макроса PARALLEL_DO(), первые четыре аргумента которого – это переменная цикла, диапазон и шаг ее изменения, как в стандартном цикле for, а пятый – длина (число строк) распределенного массива, управляющего распределением витков цикла по процессам.

Текст программы из предыдущего примера, переписанный с использованием параллельного цикла, приводится ниже.

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include <coarray.h>
#include <shared.h>
#define LSECT 10
#define LWIDTH 5
    COARRAY_MEM_ALLOC( 2000000000l );
    int main( int argc, char *argv[] )
{
    int i, j, k, my_node, n_nodes;
//    double **coarray, **vcoarray;
    long requested, used;
    FILE *fp;
/***/
    COARRAY_Init( &argc, &argv );
    my_node = coarray_my_node();
    n_nodes = coarray_n_nodes();
    COARRAY_Create_vectorized( double, vcoarray, LSECT*n_nodes, LWIDTH,
                                                         100.0, coarray );
    if ( !vcoarray )
     {
      fprintf( stderr, "No memory\n" );
      exit( -1 );
     }
    coarray_set_name( (void**)coarray, "This is my coarray" );
    if ( my_node == 0 )
     {
      fp = fopen( "output.dat", "w" );
      fclose( fp );
     }
    NODE_BY_NODE_BEGIN( i, 0, n_nodes )
     fp = fopen( "output.dat", "a" );
     if ( i == 0 ) fprintf( fp, "BEFORE ASSIGNMENT\n" );
     fprintf( fp, "Hello, I am %d of %d\n", my_node, n_nodes );
     fprintf( fp, "My view of the coarray is:\n" );
     for ( j = 0; j < LSECT*n_nodes; j++ )
      {
       for ( k = 0; k < LWIDTH; k++ )
        {
         fprintf( fp, " %f ", vcoarray[j][k] );
        }
       fprintf( fp, "\n" );
      }
     fclose( fp );
    NODE_BY_NODE_END
    coarray_barrier();
    PARALLEL_DO( i, 0, LSECT*n_nodes, 1, LSECT*n_nodes )
     {
      for ( j = 0; j < LWIDTH; j++ ) vcoarray[i][j] = i+10*j;
     }
    coarray_barrier();
    NODE_BY_NODE_BEGIN( i, 0, n_nodes )
     fp = fopen( "output.dat", "a" );
     if ( i == 0 ) fprintf( fp, "AFTER ASSIGNMENT\n" );
     fprintf( fp, "Hello, I am %d of %d\n", my_node, n_nodes );
     fprintf( fp, "My view of the coarray is:\n" );
     for ( j = 0; j < LSECT*n_nodes; j++ )
      {
       for ( k = 0; k < LWIDTH; k++ )
        {
         fprintf( fp, " %f ", vcoarray[j][k] );
        }
       fprintf( fp, "\n" );
      }
     coarray_report( (void**)coarray, &requested, &used );
     fprintf( fp, "Coarray %s requested %ld bytes, actually used %ld bytes\n",
              coarray_get_name( (void**)coarray ), requested, used );
     fclose( fp );
    NODE_BY_NODE_END
    COARRAY_Finalize();
    return 0;
}

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

Отметим также, что параллельный цикл RefNUMA, в отличие, например, от параллельного цикла OpenMP, не подразумевает в конце работы неявного барьера. Если барьер после параллельного цикла требуется, его следует записать явно, в противном случае никакой синхронизации процессов при выполнении параллельного цикла не будет. Это позволяет, в частности, писать параллельный цикл внутри поочередного цикла, что, как мы увидим ниже, часто бывает довольно удобно.

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

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

Первый аргумент – тип элементов создаваемого двумерного массива общей памяти. В нашем случае это double. Если бы требовалось создать массив большей размерности, то его следовало бы представить как двумерный массив подмассивов-элементов, с размерностью, уменьшенной на 2, а тип подмассива-элемента определить предварительно при помощи typedef.

Второй аргумент – создаваемый блочно распределенный разделяемый массив. Обратим внимание на то, что эту переменную не требуется предварительно объявлять, вызов макроса COARRAY_Create_vectorized() объявляет ее внутри себя.

Третий и четвертый аргументы – число строк (общее) и число столбцов создаваемого двумерного массива общей памяти.

Пятый аргумент - коэффициент запаса по дополнительной системной памяти. Чтобы вычислить объем дополнительной системной памяти, задаваемый вторым аргументом при обращении к coarray_create(), макрос COARRAY_Create_vectorized() умножает размер секции на этот коэффициент, и добавляет еще несколько страниц для более корректной обработки «предельных случаев» - секций очень малого размера.

Шестой аргумент – создаваемый секционированный массив, на базе которого будет создаваться необходимый нам блочно распределенный разделяемый массив. Этот массив можно использовать, например, для обращения к функциям coarray_report() и coarray_set_name(). Как и в случае второго аргумента, эту переменную не надо объявлять, она объявляется автоматически внутри вызова макроса.

◄ Пример 5 Пример 7 ►
 
 
 
 
 
 
 
 
  Тел. +7(499)220-79-72; E-mail: inform@kiam.ru