Здравствуйте,
Данный пост — продолжение первого поста по теме.
Данный пост является краткой выжимкой из статьи «Java for High Performance Computing», которая будет представлена мной на университетской конференции Томского Политехнического.
Скалярное произведение векторов — сумма всех произведений соответствующих элементов векторов.
Для решения задачи были написаны две программы — на Си (не мной :-) и на Java.
Тестирование обеих программ производилось на суперкомпьютерном кластере «СКИФ-Политех», установленном в Томском политехническом университете и состоящем из 24 узлов по 2 процессора Intel Xeon 5150 2.66 Ghz и 8 Гб оперативной памяти на каждом под управлением Linux SuSE Enterprise версии 10.3.
В качестве наборов данных в первом случае использовались два вектора размерностью 99999999 целых элементов, инициализировавшихся случайным образом, и во втором случае два вектора по 99999999 вещественных элементов. Обе программы запускались 21 раз для каждого набора данных с изменением количества используемых ядер процессоров (от 2 до 40), по два раза в каждом случае.
Следует отметить тот момент, что обе программы:
* не оптимизированы;
* используют одну и ту же функциональность (за исключением внутренних особенностей языков).
Поэтому в комментариях всячески приветствуются дополнения.
Теория, необходимая для решения поставленной задачи
В имплементациях MPI для Си и Java существуют два различия, которые могут сначала смутить:
1) В функциях пересылки сообщений первым аргументом в Си идёт объект, в Java — обязательно одномерный массив;
2) Различная последовательность аргументов.
Для расчёта скалярного произведения векторов необходимо решить следующие задачи:
1) Создать два вектора по N элементов каждый и инициализировать значения;
2) Разделить вектора на частички, которые будут разосланы узлам;
3) Разослать частички;
4) Принять частички на узлах;
5) Произвести вычисления;
6) Отослать обратно;
7) Просуммировать и получить результат;
8) Подсчитать время, затраченное на выполнение программы.
По пунктам:
1) Создать два вектора по N элементов каждый и инициализировать значения
Для Си необходимо выделить соответствующий кусок памяти на массивы — malloc(n*sizeof(double)) и в цикле рандомом rand() инициализировать значения. Для Java достаточно просто создать массивы-вектора, объект класса Random (следует отметить, что на создание объектов уходить много времени, будьте осторожны) и, используя данный объект, инициализировать массивы-вектора.
2) Разделить вектора на частички, которые будут разосланы узлам
Для Си и Java решается одинаково:
n = total / numprocs + 1, где
N — количество частичек на один узел,
Total — длина вектора,
numprocs — количество процессов (MPI_COMM_Size) в пуле.
3) Разослать частички;
Используется функция из библиотеки MPI — MPI_Bcast, рассылающая объект по всем процессам в пуле. За спецификациями можно обращаться на сайт производителя.
В результате рассылка массивов в Java выглядит так:
где d — длина кусочка от массивов,
a — первый вектор,
b — второй вектор.
4) Принять частички на узлах
Без комментариев.
5) Произвести вычисления
6) Отослать обратно; 7) Просуммировать и получить результат;
А вот здесь интересный момент — две задачи объединим в одну. Воспользуемся редуцирующей функцией, которая сама выполнит за нас все необходимые действия — соберёт результаты и сложит их в одномерный массив (не забываем, что в реализации для Java не должно быть простых переменных!) result.
8) Подсчитать время, затраченное на выполнение программы
Для этого используются две встроенные функции, обе врапперы для стандартных функций — MPI.Wtime (wall time). Поставим вызов первой в начале программы и вычисление общего времени выполнения (не вычисления!) программы в конце.
Выводы
Несмотря на все недостатки Java и сильное различие между временем выполнения программ на Си и Java, окончательное решение о выборе того или иного языка программирования может быть принято лишь после тщательного анализа предметной области и ситуации, в которой оказалась группа исследователей. В некоторых случаях, использование Си гораздо более обосновано за счет высшей призводительности и большей ориентированности на железо (следовательно, большей оптимизации всего процесса), в то же время использование Си налагает большую ответственность на программиста, который должен быть достаточно компетентен, чтобы не выпустить ситуацию из-под контроля и не допустить возникновения критических случаев, в которых программа может «утечь» и потащить за собой всю программу. Это очень важный момент в серьёзных исследованиях.
С другой стороны, использование Java также оправдано. Несмотря на потерю производительности, проблемы с вычислениями чисел с плавающей запятой и прочему Java обладает такими достоинствами, как контроль за ситуацией виртуальной машиной, развитый инструментарий по перехвату исключительных ситуаций, низкий порог вхождения для разработки «числодробилки», отсутствие таких сложных и неоднозначных инструментов, как указатели или ручное выделение памяти — всё это может быть достаточным аргументом для выбора Java как языка программирования для разработки параллельных программ для команды исследователей, не имеющей в своем составе компетентного программиста на Си.
Программа на Си
Программа на Java
Данный пост — продолжение первого поста по теме.
Данный пост является краткой выжимкой из статьи «Java for High Performance Computing», которая будет представлена мной на университетской конференции Томского Политехнического.
Скалярное произведение векторов — сумма всех произведений соответствующих элементов векторов.
Для решения задачи были написаны две программы — на Си (не мной :-) и на Java.
Тестирование обеих программ производилось на суперкомпьютерном кластере «СКИФ-Политех», установленном в Томском политехническом университете и состоящем из 24 узлов по 2 процессора Intel Xeon 5150 2.66 Ghz и 8 Гб оперативной памяти на каждом под управлением Linux SuSE Enterprise версии 10.3.
В качестве наборов данных в первом случае использовались два вектора размерностью 99999999 целых элементов, инициализировавшихся случайным образом, и во втором случае два вектора по 99999999 вещественных элементов. Обе программы запускались 21 раз для каждого набора данных с изменением количества используемых ядер процессоров (от 2 до 40), по два раза в каждом случае.
Следует отметить тот момент, что обе программы:
* не оптимизированы;
* используют одну и ту же функциональность (за исключением внутренних особенностей языков).
Поэтому в комментариях всячески приветствуются дополнения.
Теория, необходимая для решения поставленной задачи
В имплементациях MPI для Си и Java существуют два различия, которые могут сначала смутить:
1) В функциях пересылки сообщений первым аргументом в Си идёт объект, в Java — обязательно одномерный массив;
2) Различная последовательность аргументов.
Для расчёта скалярного произведения векторов необходимо решить следующие задачи:
1) Создать два вектора по N элементов каждый и инициализировать значения;
2) Разделить вектора на частички, которые будут разосланы узлам;
3) Разослать частички;
4) Принять частички на узлах;
5) Произвести вычисления;
6) Отослать обратно;
7) Просуммировать и получить результат;
8) Подсчитать время, затраченное на выполнение программы.
По пунктам:
1) Создать два вектора по N элементов каждый и инициализировать значения
Для Си необходимо выделить соответствующий кусок памяти на массивы — malloc(n*sizeof(double)) и в цикле рандомом rand() инициализировать значения. Для Java достаточно просто создать массивы-вектора, объект класса Random (следует отметить, что на создание объектов уходить много времени, будьте осторожны) и, используя данный объект, инициализировать массивы-вектора.
2) Разделить вектора на частички, которые будут разосланы узлам
Для Си и Java решается одинаково:
n = total / numprocs + 1, где
N — количество частичек на один узел,
Total — длина вектора,
numprocs — количество процессов (MPI_COMM_Size) в пуле.
3) Разослать частички;
Используется функция из библиотеки MPI — MPI_Bcast, рассылающая объект по всем процессам в пуле. За спецификациями можно обращаться на сайт производителя.
В результате рассылка массивов в Java выглядит так:
MPI.COMM_WORLD.Bcast(d, 1, 0,MPI.DOUBLE, 0);
MPI.COMM_WORLD.Send(a,0,a.length,MPI.DOUBLE,dest,0);
MPI.COMM_WORLD.Send(b,0,b.length,MPI.DOUBLE,dest,0);
где d — длина кусочка от массивов,
a — первый вектор,
b — второй вектор.
4) Принять частички на узлах
MPI.COMM_WORLD.Recv(a,0,d[0],MPI.DOUBLE,0,0);
MPI.COMM_WORLD.Recv(b,0,d[0],MPI.DOUBLE,0,0);
Без комментариев.
5) Произвести вычисления
for (int i=0; i<d[0];i++){
sum[0]+=a[i]*b[i];
}
6) Отослать обратно; 7) Просуммировать и получить результат;
А вот здесь интересный момент — две задачи объединим в одну. Воспользуемся редуцирующей функцией, которая сама выполнит за нас все необходимые действия — соберёт результаты и сложит их в одномерный массив (не забываем, что в реализации для Java не должно быть простых переменных!) result.
MPI.COMM_WORLD.Reduce(sum,0,result,0,1,MPI.DOUBLE,MPI.SUM,0);
8) Подсчитать время, затраченное на выполнение программы
Для этого используются две встроенные функции, обе врапперы для стандартных функций — MPI.Wtime (wall time). Поставим вызов первой в начале программы и вычисление общего времени выполнения (не вычисления!) программы в конце.
Выводы
Несмотря на все недостатки Java и сильное различие между временем выполнения программ на Си и Java, окончательное решение о выборе того или иного языка программирования может быть принято лишь после тщательного анализа предметной области и ситуации, в которой оказалась группа исследователей. В некоторых случаях, использование Си гораздо более обосновано за счет высшей призводительности и большей ориентированности на железо (следовательно, большей оптимизации всего процесса), в то же время использование Си налагает большую ответственность на программиста, который должен быть достаточно компетентен, чтобы не выпустить ситуацию из-под контроля и не допустить возникновения критических случаев, в которых программа может «утечь» и потащить за собой всю программу. Это очень важный момент в серьёзных исследованиях.
С другой стороны, использование Java также оправдано. Несмотря на потерю производительности, проблемы с вычислениями чисел с плавающей запятой и прочему Java обладает такими достоинствами, как контроль за ситуацией виртуальной машиной, развитый инструментарий по перехвату исключительных ситуаций, низкий порог вхождения для разработки «числодробилки», отсутствие таких сложных и неоднозначных инструментов, как указатели или ручное выделение памяти — всё это может быть достаточным аргументом для выбора Java как языка программирования для разработки параллельных программ для команды исследователей, не имеющей в своем составе компетентного программиста на Си.
Программа на Си
#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <signal.h>
#define MYTAG 1
int myid, j;
char processor_name[MPI_MAX_PROCESSOR_NAME];
double startwtime = 0.0, endwtime;
int main(int argc,char *argv[])
{
int total, n, numprocs, i, dest;
double *a, *b, sum, result;
int namelen;
MPI_Status status;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Get_processor_name(processor_name,&namelen);
if (myid == 0) {
total = atoi(argv[1]);
}
printf("Process %d of %d is on %s\n",
myid, numprocs, processor_name);
startwtime = MPI_Wtime();
n = total / numprocs + 1;
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
a = malloc(n*sizeof(double));
b = malloc(n*sizeof(double));
if ((a == NULL) || (b == NULL)) {
fprintf(stderr,"Error allocating vectors (not enough memory?)\n");
exit(1);
}
if (myid == 0) {
for (dest=1; dest < numprocs; dest++) {
for (i=0; i < n; i++) {
a[i] = 4294967296;//rand();
b[i] = 4294967296;//rand();
}
MPI_Send(a, n, MPI_INT, dest, MYTAG, MPI_COMM_WORLD);
MPI_Send(b, n, MPI_INT, dest, MYTAG, MPI_COMM_WORLD);
}
n = total - n*(numprocs-1);
for (i=0; i < n; i++) {
a[i] = rand();
b[i] = rand();
}
} else {
MPI_Recv(a, n, MPI_INT, 0, MYTAG, MPI_COMM_WORLD, &status);
MPI_Recv(b, n, MPI_INT, 0, MYTAG, MPI_COMM_WORLD, &status);
}
printf("Process %d on node %s starting calc at %f sec\n",
myid, processor_name, MPI_Wtime()-startwtime);
sum = 0.0;
for (i=0; i<n; i++)
sum += a[i]*b[i];
printf("Process %d on node %s ending calc at %f sec\n",
myid, processor_name, MPI_Wtime()-startwtime);
MPI_Reduce(&sum, &result, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (myid == 0) {
endwtime = MPI_Wtime();
printf("Answer is %f\n", result);
printf("wall clock time = %f\n", endwtime-startwtime);
fflush(stdout);
}
MPI_Finalize();
return 0;
}
Программа на Java
import mpi.*;
import java.util.*;
public class scalar {
public static void main(String args[]){
MPI.Init(args);
double[] result = new double[1];
int me = MPI.COMM_WORLD.Rank();
int size = MPI.COMM_WORLD.Size();
double startwtime=0.0;
double endwtime=0.0;
int total = 99999999;
int[] d = new int[1];
d[0] = total/size+1;
double[] a = new double[d[0]];
double[] b = new double[d[0]];
Random r = new Random();
MPI.COMM_WORLD.Bcast(d, 1, 0,MPI.INT, 0);
if (me == 0){
startwtime = MPI.Wtime();
for (int dest=1; dest<size;dest++){
for (int i=0; i<d[0]; i++){
a[i] = r.nextDouble();
b[i] = r.nextDouble();
}
MPI.COMM_WORLD.Send(a,0,a.length,MPI.INT,dest,0);
MPI.COMM_WORLD.Send(b,0,b.length,MPI.INT,dest,0);
}
d[0] = total - d[0]*(size-1);
for (int i=0; i<d[0];i++){
a[i] = r.nextDouble();
b[i] = r.nextDouble();
}
} else {
MPI.COMM_WORLD.Recv(a,0,d[0],MPI.INT,0,0);
MPI.COMM_WORLD.Recv(b,0,d[0],MPI.INT,0,0);
}
int[] sum = new int[1];
for (int i=0; i<d[0];i++){
sum[0]+=a[i]*b[i];
}
MPI.COMM_WORLD.Reduce(sum,0,result,0,1,MPI.INT,MPI.SUM,0);
if (me == 0){
System.out.println("answer is"+result[0]+" time of calcs is equal to "+(MPI.Wtime()-startwtime));
}
MPI.Finalize();
}
}