第七项:应用并行计算提高软件性能
随着Intel和AMD不断推出多核心的CPU,一芯多核,成为越来越普遍的事情。从单核到双核,从双核到四核,再到八核等等,毫无疑问,我们开始进入一个一芯多核的时代,程序员们也不得不开始考虑如何将自己的软件并行化以充分利用多核心CPU的计算能力。当我们的项目从Visual C++ 6.0升级到Visual C++ 2010之后,自然要迎接这场挑战。不过,Visual C++ 2010已经为这场挑战做好了准备,那就是全新的并行模式库(Parallel Patterns Library)。PPL在一个比操作系统线程更高的高度对并行计算进行了抽象,让程序员们不再直接跟比较危险的线程打交道,而是在另外一个更高的抽象层次,用新的Task来表达我们对可以同时执行的多个任务的封装,使得并行计算的程序更加容易理解和开发。利用PPL,也可以将我们从Visual C++ 6.0升级而来的串行的应用程序 轻松地并行化,从而充分利用多核CPU的计算能力。
PPL主要包括并行算法和并行任务两个部分。并行算法包括parallel_for(),parallel_for_each()和Parallel_invoke(),它们可以简单地将原来非常耗时的串行执行的for循环或者是for_each()算法并行化。例如,在我们原来的代码中有这样一个耗时的将图像灰度化的for循环:
const int nSize = 256;
int nR[nSize], nG[nSize],nB[nSize];
// 将图像像素保存到数组中…
// 通过for循环,将图像像素灰度化处理
for( int i = 0; i < nSize; ++i )
nR[i] = nG[i] = nB[i] = (nR[i] + nG[i] + nB[i]) / 3.0;
在这里,整个图像灰度化处理的过程,是逐个对每一个像素进行灰度化运算的串行过程。当我们在一个多核心的CPU上运行这段代码时,它只能利用CPU的一个计算核心进行计算,浪费了宝贵的硬件资源。通过PPL中的parallel_for()算法,我们可以轻松地将这一过程并行化:
#include <ppl.h>
using namespace Concurrency;
// 利用paralle_for()算法并行化的图像灰度化算法
parallel_for(0,nSize,[&](int i)
{
nR[i] = nG[i] = nB[i] = (nR[i] + nG[i] + nB[i]) / 3.0;
});
我们可以看到,通过一个简单的parallel_for()并行算法的改写,一个原来只能单核执行的串行算法摇身一变,就成了可以多核同时执行的并行算法,从而有效的利用了计算机的硬件资源。
除了使用PPL提供的并行算法将原来的串行化应用程序并行化之外,我们还可以使用PPL提供的并行任务,实现多线程应用程序的快速开发。例如,在Visual C++ 6.0中,我们要创建一个工作者线程来完成图像灰度化的任务,为了向线程传递图像数据,我们首先需要定义一个结构体,用来表示线程将要处理的图像数据:
struct ImageInfo
{
int nR[256];
int nG[256];
int nB[256];
};
然后,我们需要创建一个线程函数来执行具体的工作,同时我们还需要利用刚才定义的结构体向线程函数传递图像数据:
{
// 向线程函数传递数据
ImageInfo* pImg = (ImageInfo*)lpParam;
// 对数据进行处理,实现图像的灰度化
for( int i = 0; i < 256; ++i )
pImg->nR[i] = pImg->nG[i] = pImg->nB[i]
= (pImg->nR[i] + pImg->nG[i] + pImg->nB[i]) / 3.0;
return 0;
}
最后,才是使用AfxBeginThread()函数创建并启动这个线程:
ImageInfo img;
// 用图像数据对结构体进行赋值…
AfxBeginThread(GrayImage,
&img);
我们可以看到,在Visual C++ 6.0中创建线程实现多线程开发是一个相当繁琐的过程,并且线程操作具有相当的危险性,稍不留意就可能导致线程死锁等严重错误。PPL并行任务的引入,使得这一切都变得非常简单。在Visual C++ 2010中,我们只需要短短的几行代码,就可以完成同样的事情:
int nR[nSize], nG[nSize],nB[nSize];
// 使用Lambda表达式声明一个任务对象
auto GrayImageTask = make_task
([&]{
for( int i = 0; i < nSize; ++i )
nR[i] = nG[i] = nB[i] = (nR[i] + nG[i] + nB[i]) / 3.0;
});
// 使用任务组执行任务
task_group tg;
tg.run(GrayImageTask);
这样,Visual C++ 2010的并行计算运行时就会根据计算机的硬件资源,自动地进行线程的创建以及线程的调度等等繁琐的事情,我们只需要坐享其成就可以了。
借助PPL中的并行算法和并行任务,我们可以轻松将Visual C++ 6.0下的单线程程序并行化,从而充分利用多核CPU的计算能力,大大提高应用程序的性能,坐享免费午餐。
第八项:将软件架构设计迁移到VC2010中
在以往的Visual Studio从低版本向高版本升级的过程中,比如从Visual Studio 6到Visual Studio 2005或者Visual Studio 2008,只要完成项目代码的升级,整个升级过程就算圆满完成了。但是当我们把一个Visual C++ 6.0的项目升级到Visual C++ 2010时,我们的升级过程还差一步才能最终完成,那就是项目架构设计的迁移。现在的Visual C++ 2010已经不仅仅是一个代码编辑器和编译器,它已经升级成为一个完整的开发平台,可以覆盖整个软件生命周期中的各项活动,无论是前期的架构设计,还是中期的代码编辑和编译,甚至包括后期的软件测试等等,都在Visual Studio的覆盖范围之内。所以我们将一个项目从Visual C++ 6.0升级到Visual C++ 2010,不仅要升级项目代码,同时还要升级跟项目相关的资料,包括项目前期的架构设计以及项目后期的测试等等。
我们都知道,软件的架构设计通常是使用UML来表达的。在Visual C++ 6.0的时代,我们通常是使用第三方的建模软件,例如Rose或者Viso,来绘制UML图,描述软件的架构设计。当我们将项目升级到Visual C++ 2010时,我们应该将这些UML图迁移到Visual C++ 2010中。这样,整个项目组可以使用同一个软件工具进行工作,项目组之内的沟通会更加流畅。
遗憾的是,Visual C++ 2010并没有提供可以自动将其他软件绘制的UML图转化为自己的UML图的工具,所以我们不得不在Visual C++ 2010中手工将原来的UML图绘制到Visual C++ 2010中。
UML图从Rose到Visual Studio
当然,Visual C++ 2010不会让架构师们白白辛苦 ,Visual C++ 2010支持多种UML图,除了可以将原来的表示软件架构设计的UML图迁移到新平台之外,作为补偿,Visual C++ 2010还提供很多额外的软件建模工具,让架构师们可以在Visual Studio中更加轻松地进行架构设计。为了帮助C++项目将架构设计更好地升级到Visual Studio 2010上来,Visual Studio团队更是在在今年6月份发布了Visualization and Modeling Feature Pack工具包中,实现了对C/C++代码的可视化功能。在安装了这个工具包后,就可以通过创建依赖项关系图(Dependency Graph)来了解和分析已有的C/C++代码工程了。从依赖关系图,我们可以清楚地查看软件各个组件之间的依赖关系,帮助程序员们更好的理解整个系统:
项目的依赖关系图
我们可以将项目的依赖关系图组层展开,展现出来的就是函数之间的调用关系,这里我们可以清楚地看到, Hilo::AnimationHelpers名字空间与Ole32.dll的依赖关系归根结底就是AnimationUtility::Initilize对CoCreateInstance的调用关系。鼠标双击图中的函数,如果有对应的代码则会自动打开代码文件,并自动定位到函数的位置,这样大大方便用户在浏览依赖关系模型时察看其对应的代码内容,也帮助我们更好的理解项目。
“三大纪律,八项注意”是保证我们的革命队伍取得最终胜利的法宝。同样的,我们这里的“三大纪律,八项注意”也必将成为我们成功将项目从Visual C++ 6.0升级到Visual C++ 2010的必胜法宝。
2011都来到了,Visual C++ 6.0也该升级到Visual C++ 2010了。