65.9K
CodeProject 正在变化。 阅读更多。
Home

开始并行编程 (OpenMP)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (61投票s)

2007 年 6 月 5 日

CPOL

7分钟阅读

viewsIcon

252989

使用 OpenMP 在多核系统上提高程序性能。

引言

并行处理一直是提高程序性能的有趣方法。最近,拥有多处理器或多核 CPU 的计算机越来越多,这使得并行处理触手可及。然而,硬件上拥有多个处理单元并不能让现有程序利用它。程序员必须主动为他们的程序实现并行处理功能,以充分利用可用硬件。OpenMP 是一组编程 API,包括多个编译器指令和一个支持函数库。它最初是为 Fortran 开发的,现在也可用于 C 和 C++。

并行编程的类型

在开始 OpenMP 之前,了解我们为什么需要并行处理很重要。在典型情况下,顺序代码将在单个处理单元上执行的线程中执行。因此,如果计算机有 2 个或更多处理器(或 2 个核心,或 1 个带超线程的处理器),则只使用一个处理器执行,从而浪费了其他处理能力。与其让其他处理器空闲(或处理其他程序的其他线程),不如利用它们来加速我们的算法。

并行处理可分为两类:基于任务和基于数据。

  1. 基于任务:将不同的任务分配给不同的 CPU 并行执行。例如,文字处理器中同时运行的打印线程和拼写检查线程。每个线程都是一个单独的任务。
  2. 基于数据:执行相同的任务,但将数据的工作负载分配给多个 CPU。例如,将彩色图像转换为灰度。我们可以在第一个 CPU 上转换图像的上半部分,而在第二个 CPU 上转换下半部分(或您拥有的任何 CPU),从而将处理时间缩短一半。

有几种并行处理的方法

  1. 使用 MPI:消息传递接口 - MPI 最适合具有多个处理器和多个内存的系统。例如,具有本地内存的计算机集群。您可以使用 MPI 在此集群中划分工作负载,并在完成后合并结果。随 Microsoft Compute Cluster Pack 提供。
  2. 使用 OpenMP:OpenMP 适用于我们台式机上的共享内存系统。共享内存系统是具有多个处理器但每个处理器共享单个内存子系统的系统。使用 OpenMP 就像编写自己的小程序线程,但让编译器来完成。可在 Visual Studio 2005 Professional 和 Team Suite 中使用。
  3. 使用 SIMD 指令:单指令多数据 (SIMD) 已在主流处理器中提供,例如 Intel 的 MMX、SSE、SSE2、SSE3,Motorola(或 IBM)的 Altivec 和 AMD 的 3DNow!。SIMD 指令是用于在 CPU 寄存器级别并行化数据处理的原始函数。例如,两个无符号字符的加法将占用整个寄存器大小,尽管此数据类型的长度只有 8 位,在寄存器中留下 24 位用于填充 0 并浪费。使用 SIMD(如 MMX),我们可以加载 8 个无符号字符(或 4 个短整型或 2 个整型)在寄存器级别并行执行。可在 Visual Studio 2005 中使用 SIMD 指令,或与 Visual C++ 6.0 的 Visual C++ Processor Pack 一起使用。

Screenshot - beginopenmp1.gif

入门

设置 Visual Studio

要在 Visual Studio 2005 中使用 OpenMP,您需要

  1. Visual Studio 2005 Professional 或更高版本
  2. 多处理器或多核系统以获得速度提升
  3. 要并行化的算法!

Visual Studio 2005 使用 OpenMP 标准 2.0,位于 vcomp.dll(位于 Windows>WinSxS> (ia64/amd64/x86)Microsoft.VC80.(Debug)OpenMP.(*) 目录)动态库中。

要使用 OpenMP,您必须执行以下操作

  1. 包含 <omp.h>
  2. 在项目属性中启用 OpenMP 编译器开关
  3. 就是这样!

Screenshot - beginopenmp2.gif

理解分叉-合并模型

OpenMP 使用分叉-合并并行模型。在分叉-合并中,会创建并行线程,并从主线程分叉出去执行操作,直到操作完成才会保留,然后所有线程都会被销毁,只留下一个主线程。

线程的分裂和合并以及最终结果的同步由 OpenMP 处理。

Screenshot - beginopenmp3.gif

我需要多少个线程?

一个常见的问题是我实际上需要多少个线程?更多的线程更好吗?我该如何控制代码中的线程数?线程数与 CPU 数有关吗?

解决问题所需的线程数通常仅限于您拥有的 CPU 数量。正如您在上面的分叉-合并图中看到的,每当创建线程时,都需要一些时间来创建线程,然后是合并最终结果和销毁线程。当问题很小,而 CPU 数量少于线程数量时,总执行时间会更长(更慢),因为花费了更多时间来创建线程,然后(由于抢占行为)在线程之间进行切换,而不是实际解决问题。每当线程上下文切换时,数据必须从内存中保存/加载。这需要时间。

规则很简单,因为所有线程都将执行相同的操作(因此优先级相同),每个 CPU(或核心)1 个线程就足够了。您拥有的 CPU 越多,可以创建的线程就越多。

OpenMP 中的大多数编译器指令都使用环境变量 OMP_NUM_THREADS 来确定要创建的线程数。您可以使用以下函数控制线程数

// Get the number of processors in this system
int iCPU = omp_get_num_procs();

// Now set the number of threads
omp_set_num_threads(iCPU);

当然,您可以在上面的代码中为 iCPU 设置任何值(如果您不想调用 omp_get_num_procs),并且您可以根据需要在代码的不同部分多次调用 omp_set_num_threads 函数以获得最大控制。如果未调用 omp_set_num_threads ,OpenMP 将使用 OMP_NUM_THREADS 环境变量。

并行 for 循环

让我们从一个简单的并行 for 循环开始。以下是将 32 位彩色(RGBA)图像转换为 8 位灰度图像的代码。

// pDest is an unsigned char array of size width * height
// pSrc is an unsigned char array of size width * height * 4 (32-bit) 
// To avoid floating point operation, all floating point weights 
// in the original grayscale formula
// has been changed to integer approximation 

// Use pragma for to make a parallel for loop 
omp_set_num_threads(threads); 

#pragma omp parallel for
for(int z = 0; z < height*width; z++) 
{ 
    pDest[z] = (pSrc[z*4+0]*3735 + pSrc[z*4 + 1]*19234+ pSrc[z*4+ 2]*9797)>>15; 
}

#pragma omp parallel for 指令将根据设置的线程数并行化 for 循环。以下是在 1.66GHz Core Duo 系统(2 核)上处理 3264x2488 图像获得的性能。

Thread(s) : 1 Time 0.04081 sec 
Thread(s) : 2 Time 0.01906 sec 
Thread(s) : 4 Time 0.01940 sec 
Thread(s) : 6 Time 0.02133 sec 
Thread(s) : 8 Time 0.02029 sec

如您所见,在双核 CPU 上使用 2 个线程执行问题,时间缩短了一半。但是,随着线程数量的增加,性能并没有提高,因为分叉和合并的时间增加了。

并行双 for 循环

上述相同的问题(颜色转换为灰度)也可以通过双 for 循环的方式编写。可以这样写

for(int y = 0; y < height; y++) 
    for(int x = 0; x< width; x++) 
     pDest[x+y*width] = (pSrc[x*4 + y*4*width + 0]*3735 + 
	pSrc[x*4 + y*4*width + 1]*19234+ pSrc[x*4 + y*4*width + 2]*9797)>>15; 

在这种情况下,有两种解决方案。

解决方案 1

我们已经使用并行 for 指令使内层循环并行化。您可以看到,当使用 2 个线程时,执行时间实际上增加了!这是因为,对于 y 的每次迭代,都会执行一个分叉-合并操作,最终导致执行时间增加。

for(int y = 0; y < height; y++) 
{ 
    #pragma omp parallel for
    for(int x = 0; x< width; x++) 
    { 
    pDest[x+y*width] = (pSrc[x*4 + y*4*width + 0]*3735 + 
	pSrc[x*4 + y*4*width + 1]*19234+ pSrc[x*4 + y*4*width + 2]*9797)>>15; 
    } 
}
Thread(s): 1 Time 0.04260 sec 
Thread(s): 2 Time 0.05171 sec

解决方案 2

与其使内层循环并行化,不如使外层循环并行化是更好的选择。这里引入了另一个指令——私有指令。私有子句指示编译器使变量私有化,因此不会执行多个变量副本。在这里,您可以看到执行时间确实减少了一半。

int x = 0; 
#pragma omp parallel for private(x) 
for(int y = 0; y < height; y++) 
{ 
    for(x = 0; x< width; x++) 
    { 
    pDest[x+y*width] = (pSrc[x*4 + y*4*width + 0]*3735 + 
	pSrc[x*4 + y*4*width + 1]*19234+ pSrc[x*4 + y*4*width + 2]*9797)>>15; 
    } 
}
Thread(s) : 1 Time 0.04039 sec 
Thread(s) : 2 Time 0.02020 sec

了解线程数

在任何时候,我们都可以通过调用函数来获取正在运行的 OpenMP 线程数

int omp_get_thread_num(); 

摘要

通过使用 OpenMP,您可以免费获得多核系统的性能提升,而无需编写太多代码,只需一两行。使用 OpenMP 没有任何借口。好处显而易见,代码也很简单。

还有其他一些 OpenMP 指令,例如 firstprivate、critical sections 和 reductions,这些将在另一篇文章中介绍。 :)

关注点

您也可以使用 Windows 线程执行类似的操作,但由于同步和线程管理等问题,实现起来会更加困难。

其他说明

原始的 RGB 到灰度公式为 Y = 0.299 R + 0.587 G + 0.114 B

参考文献

  1. Michael J. Quinn, McGraw Hill, 2004. *C 语言 MPI 和 OpenMP 并行编程*(第 1 版)
  2. L. Ridgway Scott, Terry Clark, Babak Bagheri, Princeton University Press, 2005. *科学并行计算*
  3. W.P. Petersen, P. Arbenz, Oxford University Press, 2004. *并行计算导论:使用 An W.P. Petersen, P. Arbenz 的实践指南*。
  4. Gaurav Sharma, CRC Press, 2003. *数字彩色成像手册*

历史

  • 2007 年 6 月 6 日 - 添加 - MPI 功能随 Microsoft Compute Cluster Pack 提供
  • 2007 年 6 月 6 日 - 初始文章(这是我在 CodeProject 的第一篇文章!)
© . All rights reserved.