现在的位置: 首页 > 综合 > 正文

积极准备、谨慎行动——应对多核编程革命

2013年03月21日 ⁄ 综合 ⁄ 共 4257字 ⁄ 字号 评论关闭

 本文发表于《程序员》杂志2007年4月刊,版权所有,如蒙转载,敬请保留作者等版权信息,违者必究!

本文发表于恋花蝶的博客(http://blog.csdn.net/lanphaday)

 

积极准备、谨慎行动

——应对多核编程革命

广州网易 赖勇浩

多核革命

2001年,IBM推出了基于双核的Power4处理器;随后SunHP都先后推出了基于双核架构的UltraSPARC IV以及PA-RISC8800处理器。但这些面向高端应用的RISC处理器曲高和寡,并没有能够引起广大群众的关注。直到2005年第二季度,Intel发布了基于X86的桌面双核处理器,从此多核才走进平常百姓家。

在今天多核处理器已占据了越来越多的市场份额,作为一线的编程人员,我们必须直面多核革命带来的冲击。多核编程,既是机遇也是挑战,如何在这个行业大变革中把握方向、与时俱进,成为摆在我们面前的迫切课题。因为从单核到多核并不像处理器时钟频率的提升那样对程序员而言是透明的,如果我们的编写的程序没有针对多核的特点来设计,那就不能完全获得多核带来的性能提升。在这个新旧交替的战国时代,我们有什么选择、能否借鉴以前的开发经验?

是的,人类最为伟大的技能就是能够借鉴之前的经验。我们应该借鉴前人的经验,积极学习并行编程技能同时在实际工作中小心求证、谨慎行动。多核,特别是双核,与双路SMP(对称多处理器)架构非常相似:


1 IntelAMD的双核CPU结构示意图

1可以看到尽管IntelAMD的双核技术有所不同,但仍然可以发现所谓双核处理器就是将两个运算核心集成在一个处理器上。这跟在一块主板上集成两颗处理器的双路SMP系统相当相似,不同之处仅在于双核系统两个计算核心之间相互交换数据并不需要通过前端系统总线(FSB),而双路系统的两个处理器是通过FSB来交换数据的,这也是我们编写程序时需要注意的一个小细节。

       就像针对SMP编程一样,针对多核处理器编程也必须使用多线程或者多进程的形式来编写应用程序才能够得到多核带来的性能提升。可见我们在SMP并行编程上积累的经验大多都可以应用到多核编程上来。

编程的变革

       多核时代的到来,给我们的编程思维带来了巨大的冲击。为了能够充分地利用多核性能,我们必须学会以分块的思维设计程序、以多进程或多线程的形式来编写程序。到底应该使用多进程还是多线程的形式来编写程序是最让程序员感到困惑的问题之一,我觉得需要根据具体的应用来决定;但通常情况下使用多线程进行多核编程比使用多进程有更大的优势:

A)     线程的创建和切换开销比进程更小。

B)      线程间通信的方式多而且简单也更有效率。

C)     多线程有汗牛充栋的基础库支持。

D)     多线程的程序比多进程的程序更容易理解和修改。

除了编程形式,我们使用多线程编程的动机也发生了改变。在以往,对于Windows程序员来说,使用多线程的主要原因之一是为了提高用户体验:如在长时间的计算中提高UII/O或者网络的响应速度。而在多核时代我们编写应用程序为了充分利用多个计算核心,缩短计算时间或者在相同的时间段内计算更多任务。如在游戏编程时通过多线程的方式把碰撞检测的计算分散到多个CPU内核可以大大缩减计算时间;也可以利用多核做更细致的检测计算,从而能够模拟更加真实的碰撞。

在多核时代,我们对编程语言的选择也要更加谨慎。这一小节的内容虽然是个人见解但的确值得系统开发、游戏开发甚至Web开发程序员一起探讨。无论开发何种项目,相对于C/C++/Fortran等编译型语言,C#/java/Python等脚本语言也许是更好的选择。原因在于脚本语言比较高级,一般都提供了对多线程的原生支持;如C#System.Threading.Threadjavajava.lang.ThreadPythonThreading.Thread。相形之下,编译型语言往往都是通过平台相关的库来提供多线程支持,如Win32 SDKPOSIX threads等。没有统一的标准,造成使用C/C++编写多线程程序需要考虑更多的细节,提高了项目成本。从现在来看,C/C++的用户虽然不少,但在多核时代脚本语言会更受欢迎,因为船小好调头啊,脚本语言一般都没有ISO标准,说改就可以改,很快就会出现针对多核的解释器和编译器了。不过PHP/Ruby/Lua等脚本语言就会比较难得到多核程序员们的宠爱了——因为它们并没有提供内核级线程支持,它们的多线程是用户级的甚至不支持线程,用它们编写的多线程程序仍然无法完全利用多核优势。

1 各种语言对多线程支持的比较

 

C/C++等编译型语言

C#/java/Python等脚本

PHP/Ruby/Lua等脚本

语言支持多线程

库支持多线程

支持内核级线程

支持用户级线程

可模拟

可模拟

线程编程复杂度

一般/

N/A

推荐度

★★★☆

★★★★

★☆

虽然C/C++在多线程编程方面因为没有从语言级提供支持而失去了部分优势;但因为当前的主流操作系统都以C语言接口的方式提供创建线程的API,而C/C++又有相当丰富的程序库,也就一定程度上弥补了语言上的不足。使用C/C++编写多线程程序不仅可以使用Win32 SDK,还可以使用POSIX threadsMFCboost.thread等。虽然这些库都提供了一定程度的封装,减轻了程序员进行多线程负担,但对于目标定位于提升计算密集型程序的性能的多核程序员来说,这些方式仍然太为复杂。因为使用这些库几乎要增加一倍的关键代码,相应地调试和测试的成本也大大增加。更好的选择应该是使用OpenMP这种通过编译器加强来支持多线程的基础库。OpenMP通过使用#pragma编译器指令来指定并行代码段,对程序的改动相当少;而且可以指定编译为串行版本以方便调试,更可以和不支持OpenMP的编译器共存。

可见即便脚本语言在语言层次上提供了对多线程编程的原生支持,但却并没有比C/C++领先多远。根本原因在于脚本语言的基础——数据结构与算法的基础库CRT/STLC/C++基础库然一样是以串行形式来设计开发的。针对多核编程去修改基础库这一几乎所有编程语言都需要面对的燃眉之急是拉开两大阵营领先优势的生死之战,而所有权集中于某一公司或者组织的C#/java/Python这类脚本语言船小好调头,估计将赢得这场关键之役。这就是我在上文推荐选择使用脚本来编写程序的原因之一。

多核程序设计

       随着时间推进,我们终将需要面对多核系统来设计程序。多核编程我个人认为基本上等同于共享内存的并行编程,多核程序设计可以借鉴以往并行编程的经验——如分块的设计思维、并行设计方法论和多样的并行支持方式。

首先我们来谈谈分块的设计思维。因为线程是操作系统分配CPU资源的最小单位,所以如果想要设计多核并行的程序,那么我们就要形成将程序分块的设计思维。还记得初中课本上 华罗庚先生的《统筹方法》吗?现在我们可以借助华老的这篇文章来谈谈怎么样去分块:

比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶茶杯要洗;火生了,茶叶也有了。怎么办?

办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。

办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了泡茶喝。

办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。

哪一种办法省时间?我们能一眼看出第一种办法好,后两种办法都窝了工。

假定华老有两个机器人给他泡茶喝,那最好的方法显然是按照“办法甲”分工:机器人A去烧水,机器人B洗茶具;等水开了,泡茶喝。看,不经意间,我们就应用了分块的思维——把不相关的事务分开给不同的处理器执行。再举个我们工作中经常遇到的例子:有数据类型为T的序列A,求序列中值与K相等的元素个数。实现这个功能的C++函数如下:

代码 1 统计序列中值为K的元素个数

template

size_t Count(const T& K, const T* pA, int num)

{

              size_t cnt = 0;

              for(int i = 0; i

                      if(pA[i] == K) ++cnt;

              return cnt;

}

代码 1 统计序列中值为K的元素个数中显而易见Count(k, p, n) = Count(k, p, n/2) + Count(k, p+n/2, n-n/2),即序列中值等于K的元素个数为前半段中值为K的元素个数加上后半段中值等于K的元素个数。如果我们开启两条线程,一条统计前半段(执行Count(k, p, n/2)),另一条统计后半段(执行Count(k, p+n/2, n-n/2)),那么在双核系统上我们将可以节省一半的运行时间(忽略生成线程的开销等)。

      以上分块的思维都是简单直接的,如果是复杂的任务,就不可能容易地找出分块的方案了,所以需要并行设计的方法论来指导我们。经过几十年的并行程序研究,前人已经总结出若干行之有效的并行设计方法,在这里介绍一个经典的方法:数据相关图。仍然以《统筹方法》中经典的泡茶为例,我们可以画出以下数据相关图:

 

2 《统筹方法》中办法甲的数据相关图

2 《统筹方法》中办法甲的数据相关图中可以看出数据相关图是一个有向图,其中每个顶点代表一个要完成的任务;箭头表示箭头指向的任务依赖于引出箭头的任务,如果数据相关图中没有从一个任务到另一个任务的路径,那么这两个任务不相关,可以并行处理。如果华老自己动手泡茶喝,那 2 《统筹方法》中办法甲的数据相关图中红色虚框的部分是可以并行的;而如果华老有两个机器人帮他泡茶,而且有不少于2个水龙头供机器人使用,那绿色虚框的部分都可以并行而且能取得更高的效率。可见能够合理利用的资源越多,并行的加速比率就越高

      在数据相关图中,如果有不相关的任务对数据集的不同元素进行相同的操作,我们称这种数据相关表现了数据并行性。如在科学计算中经常会对某一N维向量乘以一个实数值:

for( int i = 0; i

      v[i] *= r;

如果有N个处理器,那么这N次带有数据并行性的迭代可以同时执行。除了数据并行性,如果有不相关的任务对数据集的不同元素进行不同的操作,则表现了功能并行性。还有形状为简单路径或链的数据相关图意味着在处理单个问题上

抱歉!评论已关闭.