Chinaunix首页 | 论坛 | 博客
  • 博客访问: 6319183
  • 博文数量: 2759
  • 博客积分: 1021
  • 博客等级: 中士
  • 技术积分: 4091
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-11 14:14
文章分类

全部博文(2759)

文章存档

2019年(1)

2017年(84)

2016年(196)

2015年(204)

2014年(636)

2013年(1176)

2012年(463)

分类:

2012-07-18 00:42:35

我们已经知道如何发送与接收由单个MPI内置类型的连续存储在内存的数据组成的消息。但数据很少会这样规则,你很可能需要传送由程序定义的混合类型的数据集合,或分散在内存的数据。


最简单的方式--多个消息

概念上讲,最简单的方式是标识你数据中最大的属于同构类型的并连续存储在内存里的块,然后把这些块作为单独的消息发送。

例如,一个存储在二维数组里的矩阵,你想要向另一个进程发送它的一个矩形子矩阵。在C语言中,数组的行是连续存储在内存中的。对于m行n列,起始位置为(k, l)的子矩阵,我们可以:

for (i = 0, i < m; ++i) {

    MPI_Send(&a[k+i][l], n, MPI_DOUBLE, dest, tag, MPI_COMM_WORLD);

}

如果接收进程不知道N, M, K, 或l的值,那么它们可以通过独立的消息发送。

1、这种方式的最大的好处就是你不需要学习任何新东西就可以应用它。

2、最大的坏处,就是它的开销。发送和接收消息会有固定的开销,如果一个长消息被拆分为多个短消息,那么程序性能会大大降低。

如果相关的代码执行的频度很低,且额外的消息数量很少,那么这些额外的开销就可以忽略不计。但对多数程序而言,限制自己使用这个方式不太带来任何好处。


另一个简单的方式--把数据拷贝到缓冲

把分散的数据拷贝到连续的缓冲里:

p = &buffer;

for (i = 0; i < m; ++i) {

    for (j = 0; j < n; ++j) {

        *(p++) = a[i][j];

    }

}

MPI_Send(p, n*m, MPI_DOUBLE, dest, tag, MPI_COMM_WORLD);


这种方式消除了前面一种方式的过度消息,但占用了额外的内存和CPU时间来执行到缓冲的拷贝。这种方式的很明显的限制是它一次仍只能处理一种类型。


一种诱人却错误的扩展缓冲的方式

很多时候可以把一种类型的值编码为另一种类型的值。在子矩阵例子里,我们可以把m、n、k、l的值转换成浮点数,以便把它们包含在缓冲里。然而,这样的转换通常比简单的拷贝占用更多的CPU时间。在多数情况下,结果会占用更多的内存。

这 时,你可能想用编程花招(比如转型指针类型)来把某个类型的值以位存储的方式放在另一种类型声明的缓冲里。这种方式非常危险。如果你写一个测试程序来试验 的话,它很可能会“工作”。但是,如果你广泛使用这种方式,特别在多种不同的环境下运行程序时,最终会不可避免的出错。如果幸运的话,它的错误会很引人注 目。但如果不幸运的话,你只是得到不正确的结果,而没有意识到错误的存在。

这里最基本的问题在于MPI传送的是值,而不是“位”。只要你使 用一堆以相同方式表示值的处理器,那么MPI会优化传输,简单地用“位”来传输,使得你以为它工作了。如果通信中有处理器使用不同的方式来表示部分或全部 的值时,MPI把消息里的值转换成标准的中间格式,传递这些中间格式的位,然后在另一个处理器上把中间格式转换为值。额外的转换保证了接收到的值和发送的 值相同。然而,接收处理器的值可能和原始类型的值不再有相同的位表示。


缓冲正确的方式--打包你的麻烦

MPI_Pack 例程允许你用“正确的方式”填充缓冲。MPI_Pack有描述你正填充的缓冲的参数,还有最简单方式里提供给MPI_Send的参数。MPI_Pack把 数据拷贝到缓冲里,并在必要时把它转换成标准中间格式。在所有要发送的数据都通过MPI_Pack放入缓冲后,你可以发送这个缓冲(指明类型为 MPI_PACKED),而后便不会有转换执行。


MPI_Pack函数的原型:

int MPI_Pack(void *inbuf, int incount, MPI_Datatype datatype, void *outbuf, int outcount, int *position, MPI_Comm comm);


之前的子矩阵例子变为:

int count = 0;

for (i = 0; i < m; ++i) {

    MPI_Pack(&A[k+i][l], n, MPI_DOUBLE, buffer, bufsize, &count, MPI_COMM_WORLD);

}

MPI_Send(buffer, count, MPI_PACKED, dest, tag, MPI_COMM_WORLD);


count被初始化为0,在每次打包后它都会增加打包的尺寸。如此便可以把分散的数据打包到连续的缓冲里。

在接收方,你可以用相似的方式指定类型MPI_PACKED来接收一个缓冲而不转换。接着使用MPI_Unpack(与MPI_Recv的关系和MPI_Pack与MPI_Send的关系相同)来翻译并把数据从缓冲拷贝到你真正需要它的地方。

MPI_Unpack的函数原型:

int MPI_Unpack(void *inbuf, int insize, int *position,void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm)


由于翻译,数据在缓冲里占用和本地不同的空间量。你可以通过调用MPI_Pack_size例程来计算你想放入缓冲的不同类型的数据所需的缓冲空间,从而保证你的缓冲有足够的大。

消 息内容里没有任何东西指明它是或不是由MPI_Pack创建的。如果打包的数据都使用相同的数据类型,那么接收方可以直接接收消息而不必用 MPI_PACKED接受再用MPI_Unpack解码。相反,使用原始内置类型发送的数据,在接收时也可以用MPI_PACKED接收再用 MPI_Unpacked解码。

MPI_Pack和MPI_Unpack带来很大的灵活性。除了支持任意数据类型混合的数据,它的增量构建 和消息解释允许消息的早期数据值来影响稍后在相同消息里出现的数据的类型、尺寸和目标。这种灵活性的主要开销在于缓冲所使用的内存和拷贝到缓冲或从缓冲拷 贝的CPU时间。如果构建一条消息需要大量的MPI_Pack调用(或解读一条消息需要大量的MPI_Unpack调用),那么额外的调用开销也是很可观 的。


“随意(On-the-fly)”打包--MPI衍生类型

你可以把MPI衍生类型设施看作把MPI打包和解包作为发送和接收操作的一部分的一种方法。打包和解包可以直接在MPI内部缓冲里完成,从而不需要:

1、用于打包和解包的显式的中间缓冲;

2、中间缓冲和通信缓冲之间的拷贝。

因此,使用MPI衍生类型而不是显式的打包和解包可以让你的程序更高效。


在发送时,你可以构建一个列表包含需要打包的数据的地址,而不是一个已经打包好的数据的列表。这个列表用于定义一个类型和发送时使用的类型。改写子矩阵的例子:

for (i = 0; i < m; ++i) {

    len_a[i] = n;

    MPI_Address(A[k+i][l], loc_a[i]);

    typ_a[i] = MPI_DOUBLE;

}

MPI_Type_struct(m, len_a, loc_a, typ_a, MY_MPI_TYPE);

MPI_Type_commit(MY_MPI_TYPE);

MPI_Send(MPI_BOTTOM, 1, MY_MPI_TYPE, dest, tag, MPI_COMM_WORLD);

MPI_Type_free(MY_MPI_TYPE);


三 个数组len_a、loc_a、和typ_a用于记录数据的长度、位置、和数据类型。MPI_Address用于得到和魔地址MPI_BOTTOM相对的 数据地址。当三个数组都填满后,MPI_Type_struct用来把消息转换成一个新的MPI类型,并存储在变量MY_MPI_TYPE里。 MPI_Type_commit用于告诉MPI使用MY_MPI_TYPE来发送或接收。MY_MPI_TYPE然后在真实的发送操作里作为一个真实的类 型使用。

特殊固定地址MPI_BOTTOM是要发送的数据的名义上的地址。这是因为MPI衍生类型规范里的地址总是作为相对地址来解释,而从MPI_Address获得的地址的值是相对于MPI_BOTTOM的。

最终,MPI_Type_free用于告诉MPI你不会再使用这个特殊的类型,所以用于表示这个类型的资源可以被释放或重用。

在接收时,你必须以相似的方式构建一个地址列表来接收消息里的数据,把这些地址转换成被提交的(committed)类型,然后在接收操作里使用这个类型。


注意:

1、作为打包和解包操作的直接替代器,MPI衍生类型操作通常更高效,但更麻烦。这是因为需要显式地创建、提交和释放MPI类型指示器。

2、如果有人对在打包好的缓冲发送时才会存在的数据进行打包(例如后续打包的值被计算到相同的变量里),那么衍生类型就丧失了它的效率优势。这里因为在发送前,你必须建立其它形式的缓冲来保存这些值。

3、相似地,如果数据要解包的地址有交集(例如消息的后续值被处理并放入到相同的变量里),那么衍生类型丧失了它的效率优势。这是因为你需要把这些值缓冲到其它地方,直到它们可以被处理。如果接收数据的地址不能提前确定,则缓冲也同样需要。

4、在消息的前面部分的值决定消息的后面部分的结构的情况下,衍生类型操作不能用来替代解包操作。在这些情况下,显式的类型缓冲将不能工作,你需要MPI_PACKED缓冲的零碎解包的灵活性。


将MPI衍生类型用于用户定义的类型

创建MPI衍生类型,然后在释放它前只使用一次,显得有些累赘。创建MPI衍生类型来描述重复访问的样式,并在之后重用这些类型。典型的例子就是用MPI衍生类型描述用户定义数据类型相关的访问。这种技术称为映射。

例如:

struct SparseElt {

    int location[2];

    double value;

};

struct SparseElt anElement;

int len_a[2];

MPI_Aint loc_a[2];

MPI_Datatype typ_a[2];

MPI_Aint  baseaddress;

MPI_Datatype  MPI_SparseElt;


MPI_Address(&anElement, &baseaddress);

len_a[0] = 2;  MPI_Address(&anElement.location, &loc_a[0]);

loc_a[0] -= baseaddress; typ_a[0] = MPI_INT;

len_a[1] = 1;  MPI_Address(&anElement.value, &loc_a[1]);

loc_a[1] -= baseaddress;  typ_a[1] = MPI_DOUBLE;

MPI_Type_struct(2, len_a, loc_a, typ_a, &MPI_SparseElt);

MPI_Type_commit(&MPI_SparseElt);


和 早先的例子一样,我们构建三个数组包含要传输的组件的长度、位置、和类型。不同的是,我们把组件的地址减去整个变量的地址,所以地址是相对于变量而不是 MPI_BOTTOM的。这允许我们在程序的任何地方使用类型指示器MPI_SparseElt来描述一个SparseElt类型的变量。

一旦类型被创建并被提交,它可以在任何内置指示器使用的地方,而不仅仅在发送和接收操作里。特别地,还包含了用于定义另一个可能包含SparseElt组件的MPI衍生类型,或对SparseElt类型的变量执行打包和解包。


其它定义MPI衍生类型的方式

MPI_Type_struct是构建MPI衍生类型的最通用的方式,因为它允许每个组件的长度、地址、类型都独立指定。有更不通用的过程可以描述访问的普遍样式,主要是使用数组。这些是:

1、MPI_Type_contiguous

2、MPI_Type_vector

3、MPI_Type_hvector

4、MPI_Type_indexed

5、MPI_Type_hindexed


MPI_Type_contiguous是最简单的,描述了内存里的连续的值序列。例如:

MPI_Type_contiguous(2, MPI_DOUBLE, &MPI_2D_POINT);

MPI_Type_contiguous(3, MPI_DOUBLE, &MPI_3D_POINT);


MPI_Type_vecter描述了几个这样均匀存储的但在内存中不连续的序列。它的函数原型为:

int MPI_Type_vector(int count,int blocklength,int stride,MPI_Datatype old_type,MPI_Datatype *newtype_p);

count为块数,blocklength是每块的长度,stride是各个块的起始位置之间的间距。

可以用它来改写子矩阵的例子:

MPI_Type_vector(m, n, N, MPI_DOUBLE, &MY_MPI_TYPE);

MPI_Type_commit(MY_MPI_TYPE);

MPI_Send(A[k][l], 1, MY_MPI_TYPE, dest, tag, MPI_COMM_WORLD);

MPI_Type_free(MY_MPI_TYPE);

N是原矩阵A[M][N]的第二维的维度。m、n是子矩阵的维度。


MPI_Type_hvector和 MPI_Type_vector相似,除了后续块之间的距离是由字节指定的而非元素。使用字节而非元素指定距离的最普遍的原因是感兴趣的元素和其它类型的 元素散布在一起。例如,你有类型为SpaseElt的数组,你可以使用MPI_Type_hvector来描述value组件的“数组”。


MPI_Type_indexed描述了长度和内存占用都会改变的序列。因为这些序列的地址以元素衡量而非字节,所以它适合于指明单个数组的任意部分。


MPI_Type_hindexed和MPI_Type_indexed相似,除了地址由字节而非元素指定。它可以指明任意数组的任意部分,只要它们都有相同的类型。


消息匹配和不匹配

就 像从消息内容里无从知道它是否由MPI_PACKED构建的一样,我们也无法知道在构建时是否使用了MPI衍生类型以及何种类型。唯一要紧的是发送方和接 收方在消息的原始值序列的表示上达成共识。因此,使用MPI_PACK构建和发送的消息可以使用MPI衍生类型来接收,或者使用MPI衍生类型发送的消息 可以用MPI_PACKED接收并用MPI_UNPACK分发。相似的,消息可以使用MPI衍生类型发送,并用另一种类型接收。

这导致了 MPI衍生类型和它的原始类型之间显著区别。如果你使用和接收时使用的相同的MPI衍生类型来发送数据,那么消息需要包含整数个那种类型,而 MPI_Get_count将用和原始数据一样的方式来工作。尽管如此,如果类型不同,你可能最后得到一个部分值。例如,如果发送进程发送4个 MPI_3D_POINT的数组(或者说总共12个MPI_DOUBLE)而接收进程以MPI_2D_POINT数组的方式接 收,MPI_Get_count将会报告有6个MPI_2D_PINT可以接收。如果5个MPI_3D_POINT被发送(15个 MPI_DOUBLE),那么在接收方有7个半的MPI_2D_POINT,但是MPI_Get_count不能返回7.5。与其返回7或 8,MPI_Get_count返回一个标志值MPI_UNDEFINED。如果你需要关于传送的尺寸的信息,那么你仍然要使用 MPI_Get_element来知道有9个原始值被传送。


控制衍生类型的长度

几种在内存里连续的衍生类型的概念在传输任意数据序列的通常情况下可能会有问题。关于这个的MPI规则被设计为和你在映射MPI衍生类型到用户定义类型的通常情况下所期望的那样工作。

首 先,MPI计算这个类型的低边界和高边界。默认情况下,更低的边界是首先出现在内存里的组件的起始位置,而高边界是最后出现在内存的组件的末尾(末尾可能 包含了反映对齐和填充规则。低边界和高边界之间的距离称作它的长度(extent)。如果该类型的两个元素之间的距离和长度相同,则它们被视为在内存中连 续。换句话说,如果第二个元素的低边界和第一个的高边界相同,那么它们连续。这种定义衍生类型长度的方法通常得到“正确”的结果。然而,有些情况也不一 定。

1、MPI库可以只实现一组填充和对齐规则。如果你的编译器有选项控制这些规则,或编译器对不同的语言使用不同的规则,那么MPI可能不经意地使用“错误”的填充和对齐规则来计算高边界。

2、如果你的MPI衍生类型只映射用户定义类型的部分组件,那么MPI可能不知道真正的第一个和最后一个元素,因此低估了长度。

3、如果你的衍生类型的组件是任意的存储序列,那么默认的长度将会几乎没有任何用处。

在 这些情况下,你可以通过在你的衍生类型定义里插入MPI_LB和MPI_UB类型的组件来控制类型的边界,从而控制类型的长度。这些组件的地址则被视为低 边界和高边界,而无视其它组件的地址。一种最简单的解决方案是为你映射的类型的数组的低边界和高边界定基址,指明数组里第一个元素为类型的低边界,元素的 第二个元素的地址为类型的高边界。

例如,你的程序里有数组X、Y、Z。有时候你想把X、Y、Z的前N个值发送到另一个进程。如果你以显式的 方式,把X的前N个元素发送出去,继而是Y的前N个元素,最后是Z的前N个元素,那么接收进程将无从知道X的元素在何处终止,Y的元素从何处开始,直到它 收到所有的值然后使用消息的长度来决定N的值。把N值放在消息的开头并不能解决这个问题,因为接收进程必须在接收消息的任何部分前定义它想把元素接收到哪 里。这个问题的解决方案是重新排列消息里的这些值,首先发送第一个X、然后第一个Y、接着第一个Z,之后是第二个X、Y、Z,继而是第三个,等等。这种排 列可以让接收进程在不提前知道总元素个数的情况下知道元素出自何处。

len_a[0] = 1;

MPI_Address(X[0], loc_a[0]);

typ_a[0] = MPI_DOUBLE;

len_a[1] = 1;

MPI_Address(Y[0], loc_a[1]);

typ_a[1] = MPI_DOUBLE;

len_a[2] = 1;

len_a[2] = 1;

MPI_Address(Z[0], loc_a[2]);

typ_a[2] = MPI_DOUBLE;

len_a[3] = 1;

MPI_Address(X[0], loc_a[3]));

typ_a[3] = MPI_LB;

len_a[4] = 1;

MPI_Address(X[1], loc_a[4]);

typ_a[4] = MPI_UB;

MPI_Type_struct(5, len_a, loc_a, typ_a, MY_TYPE);

MPI_Type_commit(MY_TYPE);

MPI_Send(MPI_BOTTOM, N, MY_TYPE, dest, tag, MPI_COMM_WORLD);

MPI_Type_free(MY_TYPE);


MY_TYPE 的格式可以工作,因为X、Y、Z都是相同的类型,所以Y[1]相对于X[1]的地址和Y[0]想对于X[0]的地址相同。注意N只用来发送,而不在 MY_TYPE的定义里,所以你可以只定义MY_TYPE一次而多次发送它,而不是每次发送时都重定义它然后释放它。


得到关于你的衍生类型的信息

一旦你定义了一个衍生类型,有几个工具调用可以给你提供该类型的信息。

1、MPI_Type_lb和MPI_Type_ub可以得到类型的低边界和高边界。

2、MPI_Type_extent可以提供类型的长度。在多数情况下,这是这个类型的一个值将占用的内存量。

3、MPI_Type_size可以提供消息里类型的尺寸。如果类型在内存中分散,那么这可能会比该类型的长度小很多。

阅读(1367) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~