多线程应用开发指南:数据依赖与并行性

在现代计算中,多线程并行性是提升性能的关键手段之一。然而,确保每个线程都能高效运行同样重要。虽然优化编译器可以完成大部分工作,但程序员通过改变源代码来提高性能的情况并不少见,这些改变通常通过利用数据重用和选择适合机器优势的指令来实现。不幸的是,那些提高串行性能的技术可能会无意中引入数据依赖,从而使得通过多线程获得额外性能变得困难。

例如,通过重复计算来避免重复计算,可以通过重用中间结果来实现。例如,通过模糊处理来软化图像,可以通过将每个图像像素替换为其邻域像素(包括它自己)的加权平均值来实现。以下伪代码描述了一个3x3模糊模板:

for each pixel in (imageIn) sum = value of pixel // 计算9个像素的平均值 for each neighbor of (pixel) sum += value of neighbor pixelOut = sum / 9

每个像素值都参与到多个计算中,这允许利用数据重用来提高性能。在下面的伪代码中,中间结果被计算并使用了三次,从而提高了串行性能:

subroutine BlurLine (lineIn, lineOut) for each pixel j in (lineIn) pixelOut = (pixel j-1 + pixel j + pixel j+1) / 3 declare lineCache[3] lineCache[0] = 0 BlurLine (line 1 of imageIn, lineCache[1]) for each line i in (imageIn) BlurLine (line i+1 of imageIn, lineCache[i mod 3]) lineSums = lineCache[0] + lineCache[1] + lineCache[2] lineOut = lineSums / 3

这种优化引入了输出图像相邻行之间的依赖关系。如果尝试并行计算这个循环的迭代,依赖关系将导致不正确的结果。

建议

最好是在不必移除现有优化或进行大量源代码更改的情况下,找到利用并行性的方法。在移除任何串行优化以暴露并行性之前,请考虑是否可以通过对整个问题的子集应用现有内核来保留优化。通常,如果原始算法包含并行性,也可以定义子集作为独立单元,并并行计算它们。

为了有效地对模糊操作进行线程化,考虑将图像细分为固定大小的子图像或块。模糊算法允许独立计算数据块。以下伪代码说明了使用图像块的用途:

// 一次性操作: Decompose the image into non-overlapping blocks. blockList = Decompose (image, xRes, yRes) foreach (block in blockList) { BlurBlock (block, imageIn, imageOut) }

现有的模糊整个图像的代码可以在实现BlurBlock时重用。使用OpenMP或显式线程对多个块并行操作,可以获得多线程的好处,并保留原始优化的内核。

使用指南

一些串行优化可以带来巨大的性能提升。考虑目标处理器的数量,以确保并行性带来的加速将超过移除优化所导致的性能损失。

引入阻塞算法有时会阻碍编译器区分别名和非别名数据的能力。如果阻塞后编译器无法确定数据是非别名的,性能可能会受到影响。考虑使用restrict关键字明确禁止别名。启用跨过程优化也有助于编译器检测非别名数据。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485