MPI中嵌套结构体的序列化与收发问题求助
问题
我正在开发一个并行程序,用于计算50000×50000像素的Julia集图像,采用MPI与PNG库实现。定义了如下结构体:
typedef struct Block { int size; int place; png_bytep *rows; } block; block knapsack; png_bytep *row_pointers;
以及内存分配函数:
void allocate() { row_pointers = (png_bytep *)malloc(sizeof(png_bytep) * height); for (y = 0; y < height; y++) row_pointers[y] = (png_byte *)malloc(sizeof(png_bytep) * width); }
编写了pack函数创建可分发的knapsack块:
void pack(int index, int size) { knapsack.rows = (png_bytep *)malloc(sizeof(png_bytep) * size); knapsack.size = size; knapsack.place = index; for (y = 0; y < size; y++) { knapsack.rows[y] = (png_byte *)malloc(sizeof(png_bytep) * width); knapsack.rows[y] = row_pointers[index + y]; } }
尝试通过以下代码收发结构体:
MPI_Send(&knapsack, sizeof(knapsack), MPI_BYTE, 1, 1, MPI_COMM_WORLD); MPI_Recv(&knapsack, sizeof(knapsack), MPI_BYTE, 0, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
但收发后指针内容不匹配,原因是指针指向的内存非连续。了解到需要序列化结构体,接触过FlatBuffers和Protocol Buffers但觉得过于复杂,也考虑过MPI_Pack但担心性能影响(最终将在128核环境测试),询问最优实现方案。
最优实现方案
方案1:自定义轻量序列化+MPI直接收发像素数据(首选)
这是128核大规模并行环境下性能最优的方案,核心是跳过指针传递,直接传输像素原始数据:
- 修复现有pack函数的内存泄漏:当前pack函数中先
malloc了knapsack.rows[y],随后直接赋值为row_pointers[index + y],导致malloc的内存无法释放,需去掉多余的malloc语句。 - 发送端流程:
- 先发送块的元数据:用
MPI_Send直接发送size和place两个int值,总共2*sizeof(int)字节。 - 发送像素数据:由于每行是单独分配的内存,可循环调用
MPI_Send发送每一行的width * sizeof(png_byte)字节;如果追求极致性能,可提前将多行数据拷贝到一块连续内存中,一次性发送(减少MPI调用次数,降低通信开销)。
- 先发送块的元数据:用
- 接收端流程:
- 先接收
size和place,确定块的大小和位置。 - 分配对应大小的内存(或提前预分配好内存池,避免频繁malloc/free)。
- 接收像素数据,将其填充到本地
row_pointers的对应位置。
- 先接收
- 性能优势:直接传输原始数据,MPI底层的RDMA等优化机制可充分发挥作用,避免序列化/反序列化的额外开销,在128核集群中能最大化利用带宽,延迟最低。
方案2:MPI派生数据类型(适合结构化数据场景)
如果希望用MPI原生机制处理结构化数据,可定义MPI派生类型,但需调整结构体设计(避免指针数组):
- 调整结构体为直接存储连续像素数据(而非指针):
typedef struct Block { int size; int place; png_byte data[]; // 柔性数组,存储size*width个像素 } block; - 创建MPI派生类型:
MPI_Datatype block_type; int blockcounts[2] = {2, size*width}; MPI_Aint displacements[2]; MPI_Datatype types[2] = {MPI_INT, MPI_BYTE}; displacements[0] = offsetof(block, size); displacements[1] = offsetof(block, data); MPI_Type_create_struct(2, blockcounts, displacements, types, &block_type); MPI_Type_commit(&block_type); - 用该派生类型直接收发结构体:
MPI_Send(&knapsack, 1, block_type, 1, 1, MPI_COMM_WORLD); MPI_Recv(&knapsack, 1, block_type, 0, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE); - 注意:此方案要求结构体数据连续,适合提前规划内存布局的场景,性能接近方案1,但灵活性稍差。
方案3:MPI_Pack/MPI_Unpack(备选,代码改动小)
如果不想大幅修改现有代码,可采用MPI_Pack/MPI_Unpack,性能虽略逊于方案1,但远优于第三方序列化库:
- 发送端:
int pack_size = 0; MPI_Pack_size(2, MPI_INT, MPI_COMM_WORLD, &pack_size); MPI_Pack_size(knapsack.size * width, MPI_BYTE, MPI_COMM_WORLD, &pack_size); char *buffer = malloc(pack_size); int position = 0; MPI_Pack(&knapsack.size, 1, MPI_INT, buffer, pack_size, &position, MPI_COMM_WORLD); MPI_Pack(&knapsack.place, 1, MPI_INT, buffer, pack_size, &position, MPI_COMM_WORLD); for (int y = 0; y < knapsack.size; y++) { MPI_Pack(knapsack.rows[y], width, MPI_BYTE, buffer, pack_size, &position, MPI_COMM_WORLD); } MPI_Send(buffer, pack_size, MPI_PACKED, 1, 1, MPI_COMM_WORLD); free(buffer); - 接收端:
// 先接收pack_size(可单独发送或约定固定大小) char *buffer = malloc(pack_size); int position = 0; MPI_Recv(buffer, pack_size, MPI_PACKED, 0, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE); MPI_Unpack(buffer, pack_size, &position, &knapsack.size, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, pack_size, &position, &knapsack.place, 1, MPI_INT, MPI_COMM_WORLD); knapsack.rows = malloc(sizeof(png_bytep) * knapsack.size); for (int y = 0; y < knapsack.size; y++) { knapsack.rows[y] = malloc(width * sizeof(png_byte)); MPI_Unpack(buffer, pack_size, &position, knapsack.rows[y], width, MPI_BYTE, MPI_COMM_WORLD); } free(buffer); - 优势:代码改动小,无需调整结构体布局;MPI_Pack/Unpack是底层优化过的接口,性能损耗可忽略,完全满足128核环境的需求。
不推荐方案:FlatBuffers/Protocol Buffers
这类通用序列化库针对跨语言、复杂数据结构设计,引入了额外的序列化开销,对于仅传输像素块的场景来说过于冗余,会降低并行程序的通信效率,不适合大规模MPI集群环境。
内容的提问来源于stack exchange,提问作者Max Rush
相关产品推荐
相关产品推荐

