3.12 派生数据类型(derived datatypes) BACKWARDFORWARD


到此为止,所有的点对点通信只牵涉含有相同数据类型的相邻缓冲区.这对两种用户限制太大.一种是经常想传送含有不同数据类型值的消息的用户(例如,一个整数计数值跟着一些实数);另一种是经常发送非连续数据的用户(例如发送矩阵的一个子块).一种解决的办法是在发送端把非连续的数据打包到一个连续的缓冲区,在接收端再解包.这样做的缺点在于在两端都需要额外的内存到内存拷贝操作,甚至当通信子系统具有收集分散数据功能的时候也是如此.而MPI提供说明更通用的,混合的非连续通信缓冲区的机制.直到执行(implementation)时再决定数据应该在发送之前打包到连续缓冲中,还是直接从数据存储区收集.

这里提供的通用机制允许不需拷贝,而是直接传送各种形式和大小的目标.我们并没有假设MPI库是用本地语言描述的连续目标.因此,如果用户想要传送一个结构或一个数组部分,则需要向MPI提供一个通信缓冲区的定义,该定义用问题模仿那个结构和数组部分的定义.这些工具可以用于使库设计者定义能够传送用本地语言定义的目标的通信函数:通过对可获得的符号表或虚拟向量(dope vector)的定义解码即可. 这种高级通信功能不是MPI的部分.

更通用的通信缓冲区可以通过用本节描述的生成器从基本数据类型中构造的派生数据类型来替换前面使用的基本数据类型来说明.这些构造派生数据类型的方法可以被应用多次.

一个通用的数据类型是一个模糊的目标,它说明两件事情:

该偏移不要求是整数,互相不同,或者升序.因此,各项的顺序可以和存储的顺序不一致,并且同样的项可以出现多次.这样的序列我们称之为"类型映像"(type map).基本类型序列(忽略偏移)是该数路类型的“类型鉴字"(type signature).

我们可以对通用数据类型使用一个句柄作为发送或接收操作的参数,来代替用基本数据类型做参数.MPI_SEND(buf,1,datatype,...)操作将用以buf作为基地址的发送缓冲区和以datatype相关的通用数据类型;它将产生一个带有由datatype参数决定的类型签名的消息.MPI_RECV(buf,1,datatype,...)将使用buf定义的基地址作为接收缓冲区地址和datatype相关的通用数据类型.

通用数据类型可以用在所有的发送和接收操作中.我们在3.12.5中讨论参数count>1的情况.

3.2.2中提供的基本数据类型是通用数据类型的特例,他们已经被预先定义好了.这样看来,MPI_INIT是一个预先定义好了的数据类型句柄,其类型映像为{(int , 0)},有一个类型入口项int和偏移0. 其它的基本数据类型与此相似.

数据类型的扩充(extent)被定义为该数据类型获得的入口项中从第一个类型到最后一个类型间的距离,循环满足分配的要求. 即如果

如果typei 要求分配ki倍数的字节地址, 则e是使extent(typemap)进入(rount)下一个maxiki所需要的最小非负增长.

例3.18 假设type={(double,0),(char,8)}(一个double型的值在偏移0,后面在偏移8处跟一个字符值).进一步假设double型的值必须严格分配到地址为8的倍数的存储空间.则该数据类型的extent是16(从9循环到下一个8的倍数).一个由一个字符或面紧跟一个双精度值的数据类型,其extent也是16.

##理论基础##

extent的定义是由这样一个假设激发的:在每个数组结构末尾添加的"充塞"(padding)数量是满足分配限制所要求的最小值.在3.12.3中提供了对extent更明确的控制.这种精确控制是为了适应上述假设不成立的情况,例如使用了union类型.

3.12.1 数据类型生成器

Contiguous(连续) 最简单的数据类型生成器是MPI_TYPE_CONTIGUOUS,`允许把一个数据类型复制到连续的位置.

MPI_TYPE_CONTIGUOUS(count,oldtype,newtype)

int MPI_Type_contiguous(int count,MPI_Datatype oldtype, MPI_Datatype *newtype)

MPI_TYPE_CONTIGUOUS(COUNT,OLDTYPE,NEWTYPE,IERROR)

newtype 是连接count份oldtype类型数据所获得的数据类型.连接由使用extent作为连接拷贝的大小来定义的.

例3.19令oldtype的类型映像为

则newtype返回的类型映像为

double和char类型元素的偏移被改为 08,16,24,32,40.

一般地,假设oldtype的类型映像为

extent = ex. 则newtype具有count.n个入口项,类型映像为

Vector(向量) MPI_TYPE_VECTOR是一个更通用的生成器,允许复制一个数据类型到含有相等大小块的空间.每个块通过连接相同数量的旧数据类型的拷贝来获得.块与块之间的空间是旧数据类型的extent的倍数.

MPI_TYPE_VECTOR(count,blocklength,stride,oldtype,newtype)

int MPI_Type_vector(int count,int blocklength,int stride,] MPI_Datatype oldtype,MPI_Datatype *newtype)

MPI_TYPE_VECTOR(COUNT,BLOCKLENGTH,STRIDE,OLDTYPE,NEWTYPE,IERROR)

例3.20 再一次假设oldtype的类型映像为{(double,0),(char,8)},extent=16.则MPI_TYPE_VECTOR(2,3,4,oldtype,newtype)调用生成的数据类型将是以下的类型映像:

即两个块,每个旧类型有三个拷贝,相邻块之间的步长stride为4个元素.

例 3.21 调用MPI_TYPE_VECTOR(3,1,-2,oldtype,newtype)将生成以下的数据类型:

一般地,假设oldtype的类型映像为

extent = ex. 设bl为blocklength.新创建的数据类型有count.bl个入口项,类型映像为

MPI_TYPE_CONTIGUOUS( count, oldtype, newtype )调用等价于调用MPI_TYPE_VECTOR( count, 1, 1, oldtype, newtype ), 或调用MPI_TYPE_VECTOR(1, count, n, oldtype, newtype), n为升序.

Hvector(异构向量)

函数MPI_TYPE_HVECTOR和MPI_TYPE_VECTOR基本相同,只是stride不再是元素个数,而是字节数.这两种生成器的使用在3.12.7中说明. H代表异构(Heterogeneous").

MPI_TYPE_HVECTOR(count,blocklength,stride,oldtype,newtype)

int MPI_Type_hvector(int count,int blocklength,MPI_Aint stride,] MPI_Datatype oldtype,MPI_Datatype *newtype)

MPI_TYPE_HVECTOR(COUNT,BLOCKLENGTH,STRIDE,OLDTYPE,NEWTYPE,IERROR)

假设oldtype的类型映像为

extent = ex. 设bl为blocklength.新创建的数据类型有count.bl.n个入口项,类型映像为

Indexed(索引)

MPI_TYPE_INDEXED允许复制一个旧数据类型到一个块序列中(每个块是旧数据类型的一个连接),每个块可以包含不同的拷贝数目和具有不同的偏移.所有的块偏移都是旧数据类型extent的倍数.

MPI_TYPE_INDEXED(count,array_of_blocklengths,array_of_displacemets,oldtype,newtype)

int MPI_Type_indexed(int count,int *array_of_blocklengths, int *array_of_displacements, MPI_Datatype oldtype, MPI_Datatype *newtype)

MPI_TYPE_INDEXED(COUNT,ARRAY_OF_BLOCKLENGTHS,ARRAY_OF_DISPLACEMENTS,OLDTYPE,NEWTYPE,IERROR)

例3.22设oldtype的类型映像为{(double,0),(char,8)},extent=16.令B=(3,1),D=(4,0),则MPI_TYPE_INDEXED(2,B,D,oldtype,newtype)调用生成的数据类型映像是:

即,旧类型的三个拷贝在偏移64处开始,一个拷贝从0偏移开始.

一般地,假设oldtype的类型映像为

extent = ex. 设B为array_of_blocklengths参数,D为array_of_displacements参数.新创建的数据类型有n.SUM(B[i],i=0,...,count-1)项,类型映像为

一个MPI_TYPE_VECTOR(count,blocklength,stride,oldtype,newtype)等价于调用MPI_TYPE_INDEXED(count,B,D,oldtype,newtype), 其中

Hindex(异构索引)

函数MPI_TYPE_HINDEXED和MPI_TYPE_INDEXED基本相同,只是array_of_displacements中的块偏移不再是旧数据类型extent的倍数,而是字节数.

MPI_TYPE_HINDEXED(count,array_of_blocklengths,array_of_displacemets,oldtype,newtype)

int MPI_Type_hindexed(int count,int *array_of_blocklengths, MPI_Aint *array_of_displacements, MPI_Datatype oldtype, MPI_Datatype *newtype)

MPI_TYPE_HINDEXED(COUNT,ARRAY_OF_BLOCKLENGTHS,ARRAY_OF_DISPLACEMENTS,OLDTYPE,NEWTYPE,IERROR)

假设oldtype的类型映像为

extent = ex. 设B为array_of_blocklengths参数,D为array_of_displacements参数.新创建的数据类型有n.SUM(B[i],i=0,...,count-1)项,类型映像为

Struct(结构)

MPI_TYPE_STRUCT是最通用的类型生成器,它能够在上面介绍的基础上进一步允许每个块包含不同数据类型的拷贝。

MPI_TYPE_STRUCT(count,array_of_blocklengths,array_of_displacemets,array_of_types,newtype)

int MPI_Type_struct(int count,int *array_of_blocklengths, MPI_Aint *array_of_displacements, MPI_Datatype array_of_types , MPI_Datatype *newtype)

MPI_TYPE_STRUCT(COUNT,ARRAY_OF_BLOCKLENGTHS,ARRAY_OF_DISPLACEMENTS, ARRAY_OF_TYPES (*),NEWTYPE,IERROR)

例 3.23 设type1的类型映像为

即两个起始于0的MPI_FLOAT拷贝后面跟一个起始于16的type1,再跟三个起始于26的MPI_CHAR拷贝.(我们假设一个浮点数占4个字节).

一般地,假设T是array_of_types参数, T[i]是一个句柄,指向

extent = ex. 设B为array_of_blocklengths参数,D为array_of_displacements参数.新创建的数据类型有n.SUM(B[i],i=0,...,count-1)项,类型映像为

3.12.2 地址和扩充函数

一个通用数据类型的偏移量是相对一些初始缓冲区地址的,这些偏移的绝对地址可以被替换掉:我们可以把它看作是零地址的相对偏移量.这个初始零地址是由常量MPI_BOTTOM指明的.这样,一个数据类型可以说明通信缓冲区内元素的绝对地址,这时buf参数传递的是MPI_BOTTOM的值.

一个位置在内存中的地址可以通过调用MPI_ADRESS获得.

MPI_ADDRESS(location,address)

int MPI_ADdress(void* location, MPI_Aint *address)

MPI_ADDRESS(LOCATION,ADDRESS,IERROR) <type> LOCATION(*)

例 3.24 对一个数组使用MPI_ADDRESS.

DIFF的值是909*sizeofreal;I1和I2的值依赖于具体执行.

##对用户的建议##

C的用户可能会试图避免使用MPI_ADDRESS而使用地址运算符&. 但是要注意&表达式是一个指针而不是地址.ANSI C不要求指针的值是目标所指的实际地址--尽管通常总是这样的. .另外在使用段地址空间的机器上的参考定义可能不一致.使用MPI_ADDRESS来"参考"C变量也保证了在这样的机器上的方便性.

以下的辅助函数提供一些派生数据类型的有用信息.

MPI_TYPE_EXTENT(datatype,extent)

int MPI_Type_extent(MPI_Datatype datatype, int *extent)

MPI_TYPE_EXTENT(DATATYPE,SIZE,IERROR)

返回一个数据类型的extent,extent的定义在公式3.1.

MPI_TYPE_SIZE(datatype,size)

int MPI_Type_size(MPI_Datatype datatype, int *size)

MPI_TYPE_SIZE(DATATYPE,SIZE,IERROR)

MPI_TYPE_SIZE返回类型签名中和datatype相关的入口项总的字节;即一个消息中将用这种数据类型产生的数据的总和.数据类型中重复出现的项则计算其重复次数.

MPI_TYPE_COUNT(datatype,count)

int MPI_Type_count(MPI_Datatype datatype, int *count)

MPI_TYPE_COUNT(DATATYPE,COUNT,IERROR)

返回指定数据类型中"顶层"项的个数.

3.12.3 下界和上界标记

定义辅助的类型映像下界和上界并且覆盖公式3.1的定义经常是很方便的.它可以使用户定义开始或结束处留有"洞穴"的数据类型, 或者定义超越上,下界的数据类型. 这种应用的例子见3.12.7. 为了实现它,我们增加两个额外的"定型数据类型",MPI_LB和MPI_UB, \他们可以分别用于标记一个数据类型的下界和上界.这些定型数据类型不占空间(extent(MPI_LB) = extent(MPI_UB) =0 ).他们不影响数据类型的大小和数目,也不影响用该数据类型创建的消息的内容.可是他们影响数据类型定义的extent, 因此影响数据类型生成器复制数据类型的输出.

例 3.25 令D=(-3,0,6); T=(MPI_LB,MPI_INT,MPI_UB), B=(1,1,1).则MPI_TYPE_STRUCT(3,B,D,T,type1)产生一个extent为9的新的数据类型,并且在偏移0处含有一个整数.这是序列{(lb,-3),(int,0),(ub,6)}定义的数据类型.如果该数据类型被MPI_TYPE_CONTIGUOUS(2, type1,type2)调用复制两次,则新产生的数据类型可以用序列{(lb,-3),(int,0),(int,9),(ub,15)}来描述.(lb 或ub型的项可以被忽略掉,如果他们出现在数据类型两端以外的位置的话.

一般地,如果

当typemap的下界被定义为

类似地,typemap的上界定义为

如果typei 要求分配ki倍数的字节地址, 则e是使extent(typemap)进入(rount)下一个maxiki所需要的最小非负增长.

至此我们给出了各种数据类型生成器的正式定义和修改了的extent的定义.

下面的两个函数用于寻找一个数据类型的下界和上界.

MPI_TYPE_LB(datatype,displacement)

int MPI_Type_lb (MPI_Datatype datatype, int *displacement)

MPI_TYPE_LB (DATATYPE,DISPLACEMENT,IERROR)

MPI_TYPE_UB(datatype,displacement)

int MPI_Type_ub (MPI_Datatype datatype, int *displacement)

MPI_TYPE_UB (DATATYPE,DISPLACEMENT,IERROR)

##基本原理##

注意3.12.6中给出的规则暗示使用包含绝对地址的数据类型参数调用MPI_TYPE_EXTENT, MPI_TYPE_LB, MPI_TYPE_UB是错误的, 除非所有的地址在同一个顺序存储区内. 因此,C 在捆绑MPI_TYPE_UB时displacement是int型而不是MPI_Aint.

3.12.4 承诺(commit)和释放

通信中的数据类型目标在使用之前必须被承诺.一个承诺的数据类型仍然可以被作为参数用在数据类型生成器中. 基本数据类型不需要承诺, 他们是"预承诺"了的.

MPI_TYPE_COMMIT(datatype)

int MPI_Type_commit(MPI_Datatype *datatype)

MPI_TYPE_COMMIT(DATATYPE,IERROR)

承诺操作用于承诺数据类型,即它的通信缓冲区正式描述,而不是缓冲区的内容.这样,在一个数据类型被承诺以后,它能够被不断用不同的起始地址重复使用,传送不断改变的缓冲区内容,或者不同缓冲区的内容.

##对执行者的建议##

系统可能在承诺时间为数据类型"编译"一个内部代理,例如,把一个密集的代理改变成一个稀疏代理,并选择最方便的通信机制.

MPI_TYPE_FREE(datatype)

int MPI_Type_free(MPI_Datatype *datatype)

MPI_TYPE_FREE(DATATYPE,IERROR)

标记datatype相关的数据类型目标为解散并设datatype为MPI_DATATYPE_NULL.任何当前正在使用该数据类型的通信都将正常结束.由释放的数据类型派生的数据类型不受影响.

例 3.26下面的程序段是一个使用MPI_TYPE_COMMIT的例子

INTEGER type1, type2

CALL MPI_TYPE_CNTIGUOUS(5, MPI_REAL, type1, ierr)

CALL MPI_TYPE_COMMIT(type1, ierr)

type2 = type1

CALL MPI_TYPE_VECTOR(3,5,4,MPI_REAL,type1,ierr)

CALL MPI_TYPE_COMMIT(type1,ierr)

释放一个数据类型并不影响另一个从这个被释放的数据类型中产生的数据类型.系统的表现就如同时输入给派生数据类型生成器的数据类型参数是用值传递的.

##对执行者的建议##

执行时可能要保存一个使用该数据类型的活动通信的参考计数, 以便决定何时释放它. 同时用户可以执行派生数据类型生成器, 让他们指向他们的数据类型参数,而不是拷贝. 在这种情况下, 用户需要保留活动数据类型的参考定义的情况, 以便知道何时可以释放一个数据类型目标.

3.12.5 使用通用数据类型通信

指向派生数据类型的句柄可以在任何要求数据类型参数的地方传递给一个通信调用. 类似MPI_SEND(buf, count, datatype,...)的调用, 当count>1时被解释成如同这个调用被传给了一个新的数据类型, 这个新的数据类型是coun个datatype拷贝的连接. 于是, MPI_SEND ( buf, count, datatype, dest, tag, comm )等价于:

MPI_TYPE_CONTIGUOUS( count, datatype, newtype )

MPI_TYPE_COMMIT( newtype)

MPI_SEND( buf, 1, newtype, dest, tag, comm ).

其它类似的含有count 和datatype参数的通信函数也适用于上面的描述.

假设一个发送操作MPI_SEND( buf, count, datatype, dest, tag, comm )被执行, 其中datatype的数据类型映像为:

并且extent=extent. ("pseudo-type"的MPI_UB 和 MPI_LB因为是空, 所以没有在上面列出,但是他们影响extent的值.) 发送操作发送n.count个项, 其中i.n+j 项在 addri,j = buf + extent.i + dispj , 且当 I = 0,..., count-1, j=0,...,n-1时类型为typej. 这些项不需要连续或分布, 他们的顺序可以指定.

在addri,j存放的变量类型应该和typej匹配(见3.3.1). \发送的消息包含n.count个项,其中i.n+j项的类型为typej.

类似地, 假设接收操作MPI_RECV( buf, count, datatype, source, tag, comm, status )被执行, 其中datatype的数据类型映像为: {(type0, disp0),..., (typen-1,dispn-1 )}, 并且extent=extent. ("pseudo-type"的MPI_UB 和 MPI_LB因为是空, 所以没有在上面列出,但是他们影响extent的值.) 接收操作接收n.count个项, 其中i.n+j 项在 buf + extent.i + dispj , 且类型为typej. 如果输入的消息含有k个元素, 则k<=n.count; 第i.n+j个元素的类型应该匹配typej.

类型匹配是根据相应数据类型的类型签名来定义的, 也就是基本数据类型部分的序列. 类型匹配并不依赖于数据类型定义的某些方面(如偏移或介质类型).

例 3.27 该例说明了类型匹配是根据一个派生数据类型含有的基本数据类型来定义的.

每个发送都和任何一个接收相匹配.

一个数据类型可以指定覆盖项. 如果这样的数据类型在接收中使用的话, 即\接收操作对部分接收缓冲区执行多次写操作, 则调用出错.

假设MPI_RECV( buf, count, datatype, dest, tag, comm, status ) 被执行, 其中datatype的数据类型映像为:

接收操作不需要填写全部的接收缓冲区,也不需要填写n的倍数的位置.任意k个基本元素都可以接收, 其中0<=k<=count.n. 接收到的基本元素个数可以用查询函数MPI_GET_ELEMENTS从status中获得

MPI_GET_ELEMENTS(status, datatype, count )

int MPI_Get_elements( MPI_Status status, MPI_Datatype datatype, int *count)

MPI_GET_ELEMENTS(STATUS,DATATYPE,COUNT,IERROR)

前面定义的函数MPI_CET_COUNT(3.2.5)功能不同.它返回接收到的"最高层元素"个数. 在前文的例子中MPI_GET_COUNT可能返回一个整数k, 0<=k<=count. 如果MPI_GET_COUNT返回k, 则接收到的基本元素个数(MPI_GET_ELEMENTS的返回值)是n.k.如果接收到的基本元素个数不是n的倍数, 即接收操作接收到的不是整数个数据类型的拷贝,则MPI_GET_COUNT返回值为MPI_UNDEFINED.

例 3.28 MPI_GET_COUNT和MPI_GET_ELEMENT的使用

函数MPI_GET_ELEMENTS也可以在执行检查操作来检查元素个数之后再调用. 注意MPI_GET_COUNT和MPI_GET_ELEMENT对基本数据类型使用时返回值相同.

基本原理

对MPI_GET_COUNT的扩充定义看上去是很自然的: 用户在填写接收缓冲区后可能希望返回count参数的值. 有时datatype代表用户想要传送的一个基本单元数据, 例如, 一个记录矩阵中的一条记录(结构). 用户将可以知道接收到了多少个部分而不必通过拿每个部分所含元素的个数去除接收到的总数来计算. 可是在其它情况下datatye用于定义一个复杂的接收区间的数据分布情况, 而不代表传送的基本数据单元. 在这种情况下用户需要使用MPI_GET_ELEMENTS.

##对执行者的建议##

从定义中可以推出, 一个接收不能改变定义的入口项以外的存储空间的值来填写通信缓冲区. 另外, 一个结构的填充区在该结构从一个进程拷贝到另一个进程时不能被改变. 因为这将阻止把拷贝结构连同填充区优化为一个连续区间. 当不影响计算输出的时候是随时可以优化的. 用户可以通过显式包含填充区作为消息的一部分来实现"强行"优化.

3.12.6 正确使用地址

在C或FORTRAN中相继定义的变量不一定存储在相连的地方. 因此需要注意对一个变量的偏移不一定对另一个变量适用. 另外, 对于使用段地址空间的机器, 地址是不统一的并且地址计算有各自的特点. 所以address(相对于MPI_BOTTOM开始地址的偏移量)的使用是有限制的

如果几个变量属于同一个矩阵则属于同一个顺序存储区, 在FORTRAN中属于同一个COMMON块, 在C 中属于同一个结构. 有效的地址定义如下:

1. 函数MPI_ADDRESS返回一个有效地址, 当作为调用程序的一个参数变量传递时.

2. 通信函数的buf参数等价于一个有效地址, 当作为调用程序的一个参数变量传递时

3. 如果v是一个有效地址, i是一个整数, 则v+i是一个有效地址, 假设v 和v+i在同一个连续存储区内.

4. 如果v是一个有效地址, 则MPI_BOTTOM+v是一个有效地址.

一个正确的程序只使用有效地址来标识通信缓冲区中入口项的位置. 而且如果u和v是两个有效地址, 则整数u-v只有当u和v在同一个连续存储区间是才可以被计算. 对于地址, 没有其它的数学运算是有效的.

上面的规则对于派生数据类型没有限制, 只要他们是用于定义的通信缓冲区完全包含在一个连续存储区内即可. 不过, 构造一个含有不在一个连续存储区的变量的通信缓冲区必须遵循某些限制. 基本地, 一个包含不在同一个连续存储区间的变量的通信缓冲区,只有通过在通信调用中说明buf=MPI_BOTTOM, count=1, 并且在所有偏移地址是有效绝对地址的地方使用datatype参数的情况下才可以使用.

##对用户的建议##

MPI执行器并不保证检测偏移量"越界"的错误, --除非覆盖了用户地址空间--因为MPI调用可能不知道本地程序中矩阵和记录的扩充(extent).

##对执行者的建议##

在一个具有连续地址空间的机器上是不需要用地址(绝对)和偏移(相对)的不同的: MPI_BOTTOM是零, 地址和篇移量都是整数. 在需要区分的机器上, 地址是靠包含MPI_BOTTOM的表达式来识别的.

3.12.7 一些例程

下面的例子说明了派生数据类型的使用.

例3.29 发送和接收一个部分三维数组。

C 提出a(1:17:2, 3:11, 2:10) 并存入e(:,:,:).

C 创建一维数据类型

C 创建二维数据类型

C 创建整个数据类型

例3.30 拷贝矩阵的严格下三角阵

C 拷贝数组a的下三角部分到数组b的下三角部分

C 计算每列开始和大小

例3.31 转置矩阵

C 转置矩阵a到b

C 为一行创建数据类型

C 以行主元顺序创建矩阵数据类型

C 以行主元顺序发送矩阵并以列主元顺序接收

例 3.32 另一个转置问题

C (转置矩阵a到b)

C 创建一行的数据类型

C creat datatype for one row, with the extent of one real number

C 发送100行以列顺序接收

例3.33 操作一个结构数组

例 3.34 和上例相同的操作, 只是数据类型中改用绝对地址.

例 3.35 处理union


Copyright: NPACT BACKWARDFORWARD