chgk 2008-1-26 22:07
(转自英特尔中国)多内核英特尔® 处理器的优化技巧
[font=黑体][color=Red][/color][/font][size=4](转自英特尔中国)多内核英特尔® 处理器的优化技巧[/size]
简介
英特尔® 软件开发产品可帮助开发人员针对多内核英特尔® 架构处理器进行编程和优化。
作者:Max Domeika 和 Lerie Kane
长久以来,英特尔一直在致力于提供更高性能的微处理器架构。现在,英特尔推出高性能平台架构解决方案,可在解决空间限制和功耗问题的同时,提供更高的处理能力。在处理器时钟速度提升之外更提供集成的平台解决方案,英特尔在推动新一代通信和嵌入式计算应用方面保持着无可置疑的领先地位。
英特尔多内核处理器的推出使得开发人员可以在大幅提高性能的同时降低功耗。为了充分实现这一潜在优势,开发人员必须了解其应用程序中的内在并行性。就此,英特尔提供了丰富的开发工具与技术信息,以协助开发人员在多内核平台上获得最大程度的性能提升。
本文简要介绍了并行结构与编程技巧,重点描述了常见线程问题以及性能调优。
充分利用并行性
为了协助开发人员发现应用程序中的并行性运用机会,英特尔提供了英特尔® VTune™ 可视化性能分析器 。
英特尔® VTune™ 可视化性能分析器是一款性能分析工具,可利用硬件中断,使开发人员能够真实了解应用程序的执行情况。该工具可在 Microsoft Windows* 以及各种 Linux* 平台上使用,非常适用于程序性能密集部分的分析。它采用了两项技术,在分析代码中的线程技巧运用机会时非常有用:取样和调用图。
由于英特尔® VTune™ 可视化性能分析器可以对处理器中近乎所有性能计数器进行取样,因此开发人员从时钟周期(clocktick)便可以了解程序的每项功能所花费的时间。一旦找到最耗时的功能,就可深入分析源代码,确定是否可有效地实施线程技术。某些资源密集型功能可能无法实施并行执行。如果您遇到一个热点无法实施线程处理,则可接着使用英特尔® VTune™ 可视化性能分析器调用图技术。调用图可以图形方式描述整个应用程序中的调用树形结构。即使某热点无法进行线程处理,该技术也可以沿着调用树向上确定可进行线程处理的某项功能。对该项功能进行线程处理,可让多个线程同时调用热点功能,从而提高性能。
有效的并行性
在系统中实施并行处理的形式有很多种,其中常用的一种就是共享内存并行性,这便意味着:
同时执行多个线程。
多个线程共享同一个地址空间,这不同于可并行执行但使用不同地址空间的多进程方式。
线程间相互协调其作业。
多个线程由底层操作系统调度,因此需要操作系统支持。
为说明有效并行性的关键所在,我们选择了一个实际生活中的例子,即多个工人给一个草坪割草。第一个要考虑的问题是怎样平均地分配工作,这种劳动力的平均分配可以使每位工人都能够尽可能地积极劳动;其次,每位工人都应有自己的割草机,否则工作效率就会大打折扣;最后,对相关物品(如油罐和储草箱)的使用需要进行协调。此例中所展示的并行性的关键要素可以概括为以下几条:
确定同时进行的工作。
平均分配工作。
对都要使用的资源创建专用副本。
同步对唯一共享资源的使用。
并行技术可分为三类,分别是线程库、消息传递库和编译器支持。线程库(如 POSIX* 线程和 Windows* API 线程)可实现对线程的显性控制;如果需要对线程进行精细管理,可以考虑使用这些显性线程技术。借助消息传递库(如消息传递接口〔MPI〕),应用程序可同时利用多台计算机,它们彼此间不必共享同一内存空间。MPI 广泛应用于科学计算领域。第三项技术是在英特尔® 编译器中实现的线程处理支持,采用的形式是 OpenMP* 和自动并行化。
Linux* 版的英特尔® C++ 编译器 9.0
英特尔® C++ 编译器是一款优化的编译器,面向多款操作系统(其中包括 Microsoft Windows* 和 Linux*)、多种架构(32位英特尔® 架构、英特尔® 安腾® 架构)和采用英特尔® 64 位扩展技术的系统。它符合 C 和 C++ 语言标准,并与 GNU 编译器集合(GCC)二进制兼容。.该款编译器的最大优势是其优化技术和性能特性支持,其中包括 OpenMP* 和自动并行化。如欲进一步了解有关英特尔编译器的更多信息,请参阅产品网页。
OpenMP* 是一种可移植的共享内存多处理应用程序接口,为多家厂商的多款操作系统所支持,可用于以下编程语言中:Fortran 77、Fortran 90、C 和 C++。OpenMP 将线程管理的许多细节和线程通信隐藏在了简化的编程接口之后,从而简化了并行应用程序的开发。开发人员可通过向源代码中添加编译指示(progma)来指定代码的并行区域。此外,这些编译指示还可传达其他信息,如变量的属性以及简单的同步处理等。
#include <stdio.h>
#include <omp.h>
static int num_steps = 100000;
double step;
#define NUM_THREADS 2
int main ()
{ int i;
double x, pi, sum = 0.0;
step = 1.0/(double) num_steps;
omp_set_num_threads(NUM_THREADS);
#pragma omp parallel for reduction(+:sum) private(x)
for (i=0;i< num_steps; i++){
x = (i+0.5)*step;
sum = sum + 4.0/(1.0+x*x);
}
pi = step * sum;
printf(“%lf\n”, pi);
}
表 1:OpenMP 代码示例,展示编译指示和库函数的用法
表 1 是一个 OpenMP 程序示例,通过对曲线下方的面积求和来计算 pi 值。如上所示,该程序除了添加的两行代码外,与代码的原始串行版本非常相似。关键的代码行是以下编译指示:“#pragma omp parallel for reduction (+:sum) private (x)”,该编译指示指定后面的 for 循环内容应由一组线程来执行,求和参数表示的临时部分结果应在并行区域的未尾通过加法进行累加。最后,变量 x 是一个私有变量(private),表示每个线程有其自己的专用副本。代码的并行处理关键概括起来有如下几点:
确定同时进行的工作。同时进行的工作是对曲线不同部分所包围面积进行计算。
平均分配工作。要计算的矩形面积数目为 100,000 个,在各个线程间平均分配。
对都要使用的资源创建专用副本。因为每个线程的拷贝各不相同,因此变量 x 必须为私有变量。
同步对唯一共享资源的使用在此示例中,唯一共享的资源(步骤)不需要同步,因为线程只读取该资源,而不对其进行写入。
自动并行化可分析循环,为确定宜于并行处理的循环创建线程处理代码。由于自动并行化所需的工作非常少,因此是在对代码进行并行处理时不错的首选技术。编译器只对确定进行并行处理是安全的循环进行并行处理。以下一些技巧可能会提高并行化的成功可能性:
使用优化报告选项。并行化优化报告(Linux 中的 -par 报告)提供了编译器对每个循环的分析汇总,以及无法并行处理循环的原因。有了这个报告,即使出现了编译器无法并行处理循环的情况,开发人员也可以使用报告中提供的信息来确定需要进行手动线程处理的区域。
尽可能地使循环的运行次数可见。如果循环的运行次数能统计出来,编译器将更有可能来并行处理这些循环。
避免在循环中调用函数。函数调用可能会对那些在编译时无法确定的循环产生影响,从而妨碍进行并行处理。
调整自动并行化所需的阈值。编译器会预估循环内发生的计算数,如果确定该数量太小,则可能不会进行并行化。通过设置阈值选项(Linux 中为 par 阈值),就可以避免这种情况。
icc –parallel –par-report3 pi.c
pi-1.c(11): warning #161: unrecognized #pragma
#pragma omp parallel for reduction(+:sum) private(x)
^
procedure: main
parallel loop: line 12
shared : { "num_steps" }
private : { "i" "x" }
first priv.: { "step" }
reductions : { "sum" }
pi-1.c(12) : (col. 2) remark: LOOP WAS AUTO-PARALLELIZED.
表 2:自动并行化编译的编译器记录
表 2 显示了使用自动并行化选项对表 1 中的代码进行编译后的结果。OpenMP 支持代码的编译和执行仅使用自动并行化就成功完成,这表明了这样一个事实,即使用英特尔编译器进行的自动并行化与 OpenMP 实施体系使用了同一底层库。例如,使用自动并行化正确实现了对 omp_set_num_thread 的调用,即使此功能是由 OpenMP API 定义的。
并行代码的正确性和高效性
一旦将线程处理引入到应用程序中,开发人员就可能要面对一系列新的编程缺陷(Bug)。其中许多缺陷是难以检测到的,需要付出额外的时间和关注以确保程序的正确运行。本文将介绍一些比较常见的线程处理问题,其中包括:
数据争用
同步
线程停顿
死锁
共享错误
如果两个线程都试图同时访问同一个资源,就会发生数据争用问题。如果两个线程间没有进行有效的通信,就不可能知道哪一个线程先访问该资源。这将导致程序运行中产生的结果不一致。例如,在一次读/写数据争用中,一个线程试图写入一个变量,同时另一个线程又试图读取该变量。读取该变量的线程获得的结果将取决于写入操作是否已经发生。数据争用的复杂之处在于其不确定性。一个程序可能能够连续一百次都正确运行,但如果将其移到系统属性稍有不同的客户系统上,线程的执行顺序就与在测试系统上的顺序不同,从而导致程序失败。
纠正数据争用的方法是同步。要同步对公用资源的访问,有一种方法是借助关键区段:在一个代码块周围放上一个关键区段,通知线程在一个时间只有一个线程进入该代码块。这样一来,就可确保所有线程按有序方式访问该资源。需要注意的是,同步虽然是一项必要且有用的技术,但也必须注意限制不必要的同步,因为同步会降低应用程序的性能。由于一次只允许一个线程访问关键区段,任何其他需要访问该区段的线程将被迫等待。这就意味着,宝贵的资源将处于闲置状态,从而对性能产生负面影响。
另一个确保正确访问共享资源的方法是锁定。在这种情况下,线程在使用资源时将锁定该资源,拒绝其他线程的访问。使用锁定时,可能会发生两个常见的线程处理错误。第一个错误是线程停顿。如果有一个线程锁定了某个特定的资源,但在离开时没有释放锁定,就会发生线程停顿现象。如果另一个线程试图访问该资源,则将被迫等待无限长时间,从而导致停顿。开发人员应确保线程在向下继续之前释放锁定。死锁与停顿类似,但是发生在使用锁定层次结构的情况下。例如,如果线程 1 锁定了变量 A,然后希望锁定变量 B;同时,线程 2 锁定了变量 B,然后试图锁定变量 A,则两个线程将出现死锁。两个线程都试图访问被另一个线程锁定的变量。一般情况下,应尽可能避免使用复杂的锁定层次结构,确保以相同的顺序获取和释放锁定。
本文将介绍的最后一个问题是共享错误。它不一定会导致程序错误,但是会影响运行性能。如果两个线程对位于同一个缓存行的数据进行操作,就会发生共享错误现象。如果一个线程更改了缓存行上的数据,则将使该数据处于未验证状态。在从内存中重新加载该缓存期间,第二个线程将被迫等待。如果重复发生这种现象(例如在循环内部),则将严重影响性能。检测共享错误的一个方法是使用英特尔® VTune™ 可视化性能分析器取样技术来对二级缓存未命中(cache miss)进行取样。如果在采用线程处理的程序中经常发生此类事件,则很可能是发生了共享错误。
英特尔® 线程检测器
对采用线程处理的程序进行调试看起来可能是项繁重的工作,但如果使用了英特尔® 线程检测器,这一工作将大为简化。此项工具作为英特尔® VTune™ 可视化性能分析器的插件提供,可在程序运行期间检测线程处理错误。如果检测到错误,就会显示出来,并将错误与源代码中相应的出错行关联起来。英特尔线程检测器的一项很有用的特性就是不必等到错误实际发生就能检测出来。例如,如前所述,数据争用由于其不确定性而很难检测。该线程检测器能够精确定位可能发生数据争用的部分,即使代码在检测的时候仍能正确执行。有效使用线程检测器的关键是确保程序在运行时有一个较好的代码覆盖范围。如果代码区域中包含永远都不会执行的错误,线程检测器将无法检测该错误。因此,必须确保程序中的所有功能都得到执行。如欲进一步了解有关使用线程检测器的信息,请参阅产品网页。
英特尔® 线程分析器
一旦解决了正确性问题,接下来就要面对性能调优问题了。英特尔® 线程分析器借用了英特尔® VTune™ 可视化性能分析器的检测技术,以帮忙对使用了 OpenMP、Windows API或 POSIX* 线程的应用程序进行性能调谐。借助该工具,开发人员可以直观地查看各条线程的性能,回答诸如以下的问题:
工作是否在各线程间平均分配?
程序运行的并行程度如何?
随着处理器数量的增加,性能的提高程度如何?
线程间的同步对执行时间有什么影响?
上述问题的回答可帮助您进一步优化自己的应用程序。例如,如果您确定线程间的工作量没有平均分配,则可更改代码,并反复测试,直到确定达到平衡。如果观察到同步时间过长,则可分析代码,看一下如何简化或安全地删除部分同步代码。(以上操作不在本文讨论的范围之内。)主要的一点是,线程分析器可让您在调谐时随时监视优化的效果。如欲了解更多信息,请参阅《英特尔® 线程检测器入门》 (PDF 151KB)。
结论
当前,芯片技术已发展到多内核处理器,以获得进一步性能提升。为了充分利用这一性能提升,提高应用程序的并行性就显得非常必要。挖掘多内核处理器潜能的最佳方法就是采用线程处理技术。英特尔提供的这些软件工具可以帮您实现这一技术转变,确保您能够针对相应的支持硬件最佳地优化您的应用。
(转自英特尔中国)