C++ 专题
使用 Visual C++ 2005 的现代语言特色编写更快的代码
原著:Stephen Toub
翻译:floatingkent
原文出处:C++:Write Faster Code with the Modern Lanugage Features of Visual C++ 2005
本文内容基于 Microsoft Visual Studio 2005 发布前的版本,原来代号为“Whidbey”。本文所包含的全部信息都可能改变。
本文讨论:
.NET C++/CLI 语法
互操作(Interop)技术
配置文件指导的优化
MSIL 优化
OpenMP 支持
增强的缓冲安全检查
本文使用了以下技术:Visual C++ .NET 2003 和 Visual C++ .NET 2005
对于 C++ 语言的爱好者来说,Visual Studio .NET 2003 中 C++ 编译器的推出,绝对令人垂涎三尺。Visual C++ .NET 2003 中有98%的成分与 C++ 标准保持一致,这使它比以往任何版本更为靠近这些标准,而且它还具有像部分模版特化等集成语言支持。他还添加了增强的缓冲安全校验和经过改进的编译器诊断功能。C++语言的开发人员就像C#和Visual Basic .NET开发人员一样,可以使用拖放窗体设计器来生成健壮的 Windows 窗体应用程序。Visual C++ .NET 2003 的编译器还包含了专门面向奔腾4以及AMD Athlon 处理器的优化。
如果你对 Visual C++ .NET 2003 感到兴奋不已,你或许会疯狂的爱上它的下一个版本 Visual C++ 2005。Visual C++ 2005 为 .NET开发提供了即优雅又强大的语法支持。其最新的优化技术可以使微软的软件产品运行速度提升30%。它的新编译模式可以确保通用语言框架(CLI)对微软.NET架构的兼容和验证,并且它面向协同工作的新模型不仅提供了本机和托管环境的无缝连接,而且还提供了对混合环境的完全控制能力。这一编译器保留了在前两个版本中提供的缓冲安全校验功能,并对其进行了加强,其中包括了给 C++ 应用程序常用的函数库的全新安全性增强版本。它提供了对 OpenMP 标准和对 Intel 和 AMD Athlon 等64位平台的支持。它解决了混合动态连接库的载入问题,并且提供了在运行时自动的清除 Double P及调用的性能缺陷。这款编译器提供的增强特色和改进功能还有很多很多。就像在 C++ 开发小组的一位开发工程师告诉我的一样,“兄弟,C++ 总算找到了属于自己的位置!”
C++/CLI 新的语法规则
在我们这些人中有多少人讨厌使用前两个版本C++的托管扩展语法并且认为其中尽是错误?有多少人认为Visual C++没有被当作基于 .NET 的头号编程语言?很明显,我们之中大多数人都这样认为(此中包括 Visual C++ 的开发小组人员,你读一下他们的网志就知道了)。Visual C++ 小组的人听到了我们的抱怨,于是他们就开始开发 Visual C++2005。从前的那个和 Visual Studio .NET 一起提供的 C++ 托管扩展就像恐龙一样消失殆尽,而随后推出的是一个经过琢磨的语言定义标准和吸引人的新语法规则。新语言的设计小组对于这个版本在语言设计方面有几个重要的目标。首先(这也许对我们这些 认为编码也是一种艺术的人来说尤为重要),他们想要保证在编写C++代码时,程序员应感到很自然,而且语言本身应该提供一种优雅的语法,它应是ISO C++标准一种纯粹的扩展。他们想让程序员轻松编写可靠的代码来支持在 SQL Server 2005 中的部分信任场景,像"一次点击"(ClickOnce)部署, 窗体设计支持和托管代码支持。他们不想给任何比C++更低级的语言以任何空间。他们想把.NET的全部强大功能带给C++,而与此同时也把C++的强大功能带给.NET。他们在各个方面都取得了骄人的成功。
新的扩展规则叫做 C++/CLI,并且现在正在标准化。要想进一步了解新的语言扩展,你可以在 C++/CLI语言标准 处下载 2003年9月21日发表的试用版基本文档。
对任何阅读以新语法规则编写的代码的人来说,最明显的就是曾经在托管扩展中常用的双下划线来定义垃圾处理类,属性和其它东西的做法已成为过去。一些这样的关键字虽然仍被保留着,还包含了一些新的关键字,但现在 它们已经不被经常使用了,它们也不会影响到代码的可读性。这些双下划线的关键字由两种新的关键字来代替:语境敏感的关键字和组合关键字。语境敏感关键字是在某一语境(译者注:即上下文)才使用的关键字,而组合关键字是在和其 它关键字一起使用的关键字。例如托管扩展中的__property关键字会被property取代(不仅如此,而且用来定义一个属性及其存取机制的全部语法规则都有了戏剧性的优化,使其声明看起来非常类似于C#的语法。(参见 Figure 1)这并不影响你在编码时将"property"看作一个变量的名称来使用。在声明某一类型的属性这一语境中,被解析为"property"的标识仅被视为一个关键字。
在新语法规则当中,类型以"修饰类"的形式声明,在这样的声明中,用一个"形容词"来描述你在创建哪一种类型的类,如下所示:
class N { /*<Char name="#133"/>*/ }; // 本机型
ref class R { /*<Char name="#133"/>*/ }; // CLR 引用类型
value class V { /*<Char name="#133"/>*/ }; // CLR 值类型
interface class I { /*<Char name="#133"/>*/ }; // CLR 接口类型
enum class E { /*<Char name="#133"/>*/ }; // CLR 枚举类型
在之前的语言版本中,类型被声明时就可以确定它的使用范围。只有本机类或结构体和托管值类型可以在栈上创建。托管引用类总是存在于托管堆当中。在 Visual C++ 2005 中,所有的类型,无论是本机的还是托管的,都将在栈上创建,它使用基于栈的彻底清除语法来完成这一功能。
要想在本机堆上实例化类型T的一个对象,可以使用"new T"的方法。这样可以返回本机上这一对象的地址指针(这是在Visual Studio .NET 2002 和 Visual Studio .NET 2003中提到的一个叫做__nogc 指针的一个概念)。要想在托管堆上实例化类型T的一个对象,Visual C++ 2005 引入了 gcnew 这一关键字,它和 new 关键字是一样的使用方法。调用"gcnew T"可以返回指向在托管堆中整个对象的一个句柄。句柄是在 Visual C++2005 中引入的一个新的结构,它类似于在托管扩展中的__gc 指针。想要在栈上实例化T类型的对象,标准的“T t;”就已经足够了。
为了公平起见,我介绍一下我是如何定义实例化的。托管引用类总是存在于托管堆当中,而本机类型总是存在于栈或者本机堆当中。当一个托管引用被声明为存在于栈上时,实际上编译器还是会在托管堆上对其进行实例化。见 Figure 2。
Figure 2 在栈上的托管引用类型
这样会出现一些问题。当我在栈上的实例超出它的使用范围时会怎样?这个实例将如何被清除掉?许多C#开发人员一直在抱怨C#语言无法彻底的清除垃圾。C#提供关键字的方法使清理IDisposable的对象非常容易,但这需要更多的代码,而且与C++开发人员所熟悉的析构函数相比则显得尤为笨拙。在C#中,安全的清理工作在默认情况下是无法进行的。清理工作需要专门的代码实现。我们以 Figure 3 中的第一个C#代码段为例。StreamReader的对象被声明为在托管堆上创建。当这个方法执行完毕之后,StreamReader的实例就没有任何引用存在了。然而,直到垃圾收集程序运行时,这个对象才会被清理掉。直到那时,所用的文件才会被关闭,在此之前,这个应用程序会一直占用此文件的开着的句柄。为了添加彻底的垃圾清除功能,你必须在使用非托管资源的类当中实现IDisposable接口。
在 Figure 3 中的第二个代码例子表示了C#新代码的样子。其实这种方法也未尝不可,而且也还算有一定的可读性。但当你开始加入更多需要清理的代码对象时,你的代码就会变得越来越难懂,而且,任何你忘记清除的对象都会在最后垃圾收集程序运行的时候给收尾模块(finalizer)线程增加负担。在此之前,你也许已对一些有宝贵的资源上了锁。这点在 Visual Basic .NET 的代码实现中显得尤为不堪(尽管 Visual Basic 2005 新增加了与 C# 相类似的 Using 语句)
Visual C++2005 现在提供给任何类型以析构函数和/或收尾模块,无论它是托管的还是本机的。在此类型为托管的情况下,编译器会将析构函数映射到IDisposable::Dispose方法。这意味着你能够用C++语言编写同样的方法。如 Figure 3 第四个代码段所示,在此中读取程序的析构函数/Dispose 方法将会自动被调用,就像你在C#中使用“Using”语句一样。当某一类型在栈上创建时,它的析构函数会在它超出其使用范围时被调用。
托管扩展的一个最大的问题是对指针的使用。指针们被用于各种各样的任务,而其情况也是复杂多变,像这样的指针行为很难被理解。在某一特定的代码段中要解读自己在和哪一种指针打交道需要一定程度上的天赋。这种复杂 性在下一个版本中会被去掉。在 Visual C++ 2005 中,指针还是原原本本的指针。它们指向稳定的对象,而你则可以用指针进行算术操作。指向对象的指针的生存期限必须由开发人员来显性指出并管理。当使用指针时,运行库不会负责对指针带来的垃圾进行清理。
现在让我们看一下 Visual C++ 2005 的设计师们是如何解决这一问题的。与 Visual Studio .NET 2003 和 Visual Studio 2005 中使用 new 操作符返回指针的方式不同,“gcnew”操作符返回一个“句柄”——一个新型结构,在语法中用“^”符号来表示。该句柄引用托管堆中的整个对象。就是说, 它们不能用来指向类型的内部,而编译器有着一系列的对句柄的限制来确保其行为,而这也可以帮助开发人员正确并安全地使用句柄。句柄不允许算术运算,也不可以被映射为空指针或是任何完整的类型。然而,星号和箭头操作符仍被用来引用 其对象。
这并不意味着你不能获得一个指向垃圾收集程序堆上的指针。和在C#中使用“&”操作符与一个固定的关键字联合体相类似,在 Visual C++ 2005 中,pin_ptr 抽象类型允许你 获得一个在托管堆上对象的固定指针。只要这个指针存在,在托管堆中的对象就会被固定住,这可以防止垃圾收集程序在收集过程中移动它。Visual C++ 2005 还加入了追踪引用操作符,用百分号(%)来注释。当开发人员了解了本机的“&”引用操作符时,大多数人都知道它是一个指向对象的指针,在使用时是由编译器来自动清除的。在大多数情况下,% 对^而言就像&对*一样。
在托管的环境下,将本机引用指向托管对象就像将本机指针指向托管对象一样危险。在指针与引用幕后的基本道理就是:被引用的对象并不会被四处移动。追踪引用和本机引用很相似,除了一点,就是追踪引用引用托管堆上的对象,并且对其进行“追踪”,即便是它们被垃圾收集程序移走。百分号操作符也用来取托管对象的“地址”,所以就像&操作符应用在本机类型返回对那个对象的指针一样,%操作符应用在托管引用类型时会返回一个那个对象的句柄。
从总体上来说,当 C++ 开发人员知道标准在控制他们的编程语言时,他们都会感到心安理得。为此,由第三方推广对此标准的采用,并保证语言本身的稳定性得到进步,这个新的语法规则采集众长而成为一个名为 C++/CLI 的 提议标准。在 2003年10月,ECMA 通过投票组织了一个特殊的任务团体,名为 TG5,其目的是为了分析和采用这一标准,就像 WG21 作为 ISO C++ 的管理团体一样。实际上,在 WG21 中的关键性人物也在 TG5 中工作。他们的计划是在 2004 年年底将其(译者注:C++/CLI)标准化。
Interop 选项
Visual C++ 7.1 提供了在 Visual Studio .NET 2003 所有成员中最好的 Interop 功能。它具有实际的能力来实现现实世界的 Interop 场景。这在 Quake II 向 .NET 框架的迁移中便是例证,(具体细节参见 http://www.vertigosoftware.com/Quake2.htm ),Visual C++ 2005 进一步扩展了这一功能。
在托管与本机环境中,要使用.NET Interop有四种主要途径:COM Interop 可以由 Runtime Callable Wrappers(RCW)与 COM Callable Wrappers(CCW)来实现。通用语言运行时(CLR)负责类型 封送(除非在极少的情况下使用定制的封送机制),并且这些调用的开销很大。你需要非常小心地尽量避免接口往来过于频繁,否则就会出现很严重的性能问题。你还需要保证你的那些包装一直是和其底层的组 件保持一致。也就是说,当你有大量本机 COM 代码而要使用一些简单的 Interop 应用时,COM Interop 是非常有用处的。
第二个 Interop 的选项是使用 P/Invoke。这由使用 DLLImport 属性来完成,其中你应在方法声明处为你要引入的函数注明属性。 封送过程是依照它如何在声明处定义来处理的。然而,DLLImport 只有在当你有代码需要从DLL导出中获得函数时才是有用处的。
当你需要在本机代码中调用托管代码时,CLR 主机也是一个方法。在这种情况下,本机应用程序必须驱动所有任务的执行:建立主机,与运行库绑定,运行主机,取得适当的 AppDomain,建立调用环境,寻找需要的集合和类,并调用在目标类上的方法。在控制 发生什么以及何时发生这一角度来讲,这无疑是最完善的解决方案之一,但这也会带来令人难以想象的枯燥并需要许多自定义的代码。
第四个办法,也有可能是最简单并最可行的方法,就是使用 C++ 的 Interop 能力。通过设置/clr 开关,编译器会生成 MSIL 而不是本机代码。唯一被生成为本机代码的是那些无法被编译为 MSIL 的代码,此中包括带有内联汇编代码块以及使用像(asm)和 Streaming SIMD Extensions (SSE)这样一些使用专用 CPU 固有特性的函数代码的操作。Quake II 就是使用 /clr 开关转向.NET的。Vertigo 软件小组花费了一天的时间将原来的由C编写的游戏代码成功的转换成了C++代码,然后设置了/clr开关。很快的,他们的代码就可以毫无问题的运行在.NET框架上了。不需要添加任何多余的二进制文件而只是简单的包含了适当的头文件,托管C++和本地C++就可以在无需任何开发人员的参与的情况下而相互调用了。编译器会处理适当代码的创建来在两种环境中自由游走。
这给C++开发人员带来了一些问题。问题之一就是现在名声名声狼籍的混合 DLL 载入问题,Visual Studio .NET 2002 和 Visual Studio .NET 2003 的用户都受此问题的影响。如果你正在运行载入锁(loader lock)中的本机代码并且你引用了一个还没有被载入的程序集中的托管类型,CLR会非常友善地帮你载入这一程序集。它是通过调用 LoadLibrary 来实现这一点的。当然,LoadLibrary会尝试去获得载入锁,这会使你碰到死锁问题。开发人员和产品经理一类的人如果听说这个问题在即将推出的版本中会得到解决的话会非常高兴的。
/clr开关对 C++ 开发人员来说是一个极好的工具,但它也有一些缺点。像我之前提到的一样,由/clr开关产生的镜像既包含本机代码又包含托管代码,这有时会造成问题。首先,这些混合镜像并不是 CLI 兼容的(举个例子来说吧,它们无法在 Rotor 上运行)。它们有本机的入口,而当你频繁跨越托管边界时就会带来极大的因转换而产生的开销。但最重要的是,这些本机入口的存在会对使用包括映射在内的代码集的工具带来极大的危害。为了使用映射来检查一个镜像,必须先载入代码集并执行它。只有在所有的初始化一起执行时,映射才能够检查元数据。不幸的是,映射无法正确的载入包含有本机入口的托管代码集。
此外,Visual Studio .NET 2003 很少生成可供验证的代码。而就算它能的话,它所花费在处理一些其它重要问题上的时间也会比较多。而 MSIL 对非验证代码由着一流的支持(你可以进行指针算术运算,间接载入和访问本机堆),经验证的代码使你能够处理一些需要部分信任的情况,其中你可以轮流使用 Visual Studio 2005 提供的丰富特色。由于在 SQL Server 2005 中的托管代码主机依赖于部分信任,所以一次点击部署(ClickOnce)也是一样。Visual C++ 2005 开发小组的一个首要目标就是让编译器能够在开发人员开发非混合和经验证的镜像产品时有所帮助。他们通过引入两个新编译器开关来实现这一点:/clr:pure 和 /clr:safe,在我向你解释如何使用这些新开关之前,我需要介绍一下 C++ 的 Interop 是如何工作的。
“运行正常”
在Visual Studio .NET 2003 中,C++ Interop 技术又被称为 IJW 或是“运行正常”。在即将推出的版本中,这被改为一个更具描述性的名字“Interop 技术”。那么它是如何“正常运行”的呢?对于每个由应用程序使用的本机方法而言,编译器同时创建了一个托管的入口和一个非托管的入口。它们中的一个有着实际上的代码实现,而另外一个是一个转发代码块,它创建适当的转化并进行任何必要的封送处理。托管入口经常被用来做实际上的代码实现,除非当这个方法的代码无法用 MSIL 表示时或者开发人员使用“#pragma unmanaged”编译器参数来迫使本机入口为实际的代码实现。
当使用一个 IJW 转发模块时(举个例子,当本机入口是转发模块时)编译器提供给模块以代码实现并通过一个偏移量或是一个引入地址表(IAT)得跳转来调入实际的代码实现。对 IJW 模块时间复杂度的估计大约介于50到300个时钟周期之间(尽管精心挑选的测试情况可以使这个数字减至只有10那末大)。当转发模块是 MSIL 时,托管的 P/Invoke 就会派上用场。P/Invoke 仅包含一个声明而没有实际的方法实现;CLR 提供了对模块的运行时支持的功能。这些转发模块通常都会比相同功用的本机实现稍微慢一点点。
如上所述,使用 IJW 的结果使每个函数都有两个入口,一个托管的和一个非托管的。但某些结构需要在编译时对这些入口进行填充(举个例子,像函数指针和虚表 vtable)。而如果在编译时,我们无法获知在运行时调用地址的托管状态,那么编译器应该如何确定选哪一个入口呢?在 Visual Studio .NET2003 中,编译器总是会选择非托管入口。当然,这会在调用函数确实是托管的时候造成麻烦,这个问题就是 Double P/Invoke 问题。(参见
Figure 4)在这种情形下,托管转换对非托管模块的调用只会导致对托管代码的调用,这样的操作会导致几个大的不必要的开销。
Figure 4 Double P/Invoke问题
Visual C++ 2005 提出了几个解决方案。第一个方案就是使用可以让你去声明一个关键字:__clrcall,它确定是否在以方法轮换基础上安置一个非托管入口。使用这个关键字来对函数声明,可以防止产生一个非托管入口(这样做的一个缺点就是这个函数就不能被本机代码直接调用了)。__clrcall 关键字也可以被放置在一个函数指针上,这样在编译器有所选择的情况下,会使这个指针挂载一个托管入口。Visual C++ 2005 提供的第二方案来解决 Double P /Invoke 问题使它使用了运行时校验和一个 cookie 来帮助运行库去决定是否非托管模块可以被忽略,从而来将调用直接转发至托管入口。然而这一特性并不一定会完全解决问题。
第三个方案是纯 MSIL。新的 /clr:pure 编译器标签告诉便一起去产生一个毫无本机结构的纯托管镜像。这样不仅产生了可应用于部分信任情况的,与 CLI 相兼容的程序块,还通过防止生成非托管模块解决了 Double P/Invoke 问题。结果是,每一个函数只有一个入口(托管入口),这样虚表vtable 和函数指针就不会被非托管入口挂载。
然而,只因为代码是与 CLI 相兼容的,并不能代表它就是经过验证的,而这对于相当代码是从共享文件中载入这种低信任度情况下是一个尤为重要的指标。在另外的一个极端,微软引入了一个更为严格的编译器标标志:/clr:safe。这对于 C++ 开发人员来讲可以说是获得经过验证代码的圣杯。引用这个开关会使编译器确保生成的程序集是完全经过验证的;任何没有经过验证的结构都会抛出编译时错误。举个例子,试图将一个整型指针编译成一个变量时,将会产生:
"int* = this type is not verifiable"
的错误,意思是“int* = 该类型没有经过验证”,并标出包含无效结构的代码行。有一些情况下使用这一极端做法是适当的。比如说,所有在 SQL Server 2005 中运行为存储过程的 C++ 代码都应以此标志编译。
Figure 5 兼容模式
Figure 5 描述了数据及代码的托管与非托管环境,并且显示出其中哪种环境是不同编译器标志的对象。不包含任何/clr标志将导致生成完全本机镜像。使用/clr标志将会形成兼具托管与非托管代码和数据的混合镜像。由使用 /clr:pure 标志生成的纯 MSIL 将不会包含任何非托管代码,尽管这并不保证是经过验证的,而且可能会包含本机类型。安全的 MSIL 是验证的最终状态,其目标只是.NET框架。一句话,这两种新的编译模式将会使以前或是不可能或是难以实现的多种情况变为现实。
优化
所有优秀的软件开发人员都想要他们的软件产品有一个好的性能,编译器编写人员是一种特殊的开发人员;他们的代码不仅要有好的性能,而且由他们写出的代码生成的代码一定也要尽可能的高效。为了这种原因,任何成功的编译器背后必须要有一个好的优化支持。在这方面,Visual C++ 2005是无可挑剔的。
Visual Studio .NET 2002 和 Visual Studio .NET 2003 由于在本机代码的性能提高方面做了很多的工作,它们加入了对 C++ 编译器一些惊人的优化。在加入了 SSE 和 SSE2 框架的同时,它们还提供了面向 Intel 奔腾4芯片的支持。最为显著的是加入了 Whole Program Optimization (WPO)[译者注:全局程序优化],它允许链接器在把每个经过编译的.cpp文件变成.obj文件时对整个程序进行优化。这些目标文件和普通的目标文件有所不同,因为与其包含本机代码,不如包含用来在编译器前端和后端相互通讯的中间语言。然后链接器就能将这些文件优化为一个大的单元,这就能提供更多的内联机会,更好的栈对齐选择和在其它不同情况下的优化中的自定义调用规范的可能性。Visual C++ 2005 使用由下至上,至上而下分析这样的新特性来改善全局程序优化(WPO),但是较大的改善是以由 Profile Guided Optimization(POGO)[译者注:配置指导的优化]这一形式出现的。这种在编译器中的提供的新功能将会对性能有所改进。
对一个编译器来说,对源代码的静态分析将会留下许多开放性问题。如果在一个if语句中比较两个变量,第一个变量比第二个变量大的频率是多少?在一个switch语句中,哪一个case被选中的次数最多?那些函数使用的最多,其中哪些代码最不经常使用?如果编译器在编译时,就知道代码在运行时该如何使用的话,它就可以在大多数情况下提供优化。这正是 Visual C++ 编译器能够做到的。
Figure 6 由配置指导的优化
POGO 的编译过程如
Figure 6 所示,第一步包含编译代码并将其链接成一个工具化构造,它具备一整套配置信息探测器。当使用全局优化的时候,由编译器生成并导入链接器的目标文件,是由中间语言而不是本机代码构成。这些探测器有两种:值探测器和计数探测器。值探测器用来构造变量控制的直方图,而计数探测器被用来追踪你在应用程序中往返某一特殊路径的次数,当应用程序运行的时候,在正常使用时,数据从所有这些探测器汇集而成,并被写入一个配置数据库。这一配置数据和原来的目标文件一起被导入链接器。链接器能够分析配置数据,决定应该采用的特殊的优化,并生成一个新的非规范化的应用程序结构。这只是一个经过编译的版本而不是一个可以用来发布给客户的规范的版本。
POGO(由配置指导的优化)是很多优化成为可能。在计数探测器的基础上,在每个函数调用处都可以使用内联。值探测器使switch和if-else语句重组来取出使用频度最高的值并且能避免在找到常用case之前就有过多不必要的尝试。代码段会被重新安排,这样最常用的代码就排在一起,而不是强迫性的在代码内进行不必要的跳转。这避免了 Translation Lookaside Buffer(TLB)[译者注:翻译查找缓冲]中高开销的吞吐量和页面交换。
不常使用的代码可以放在这个模块的一个特殊的段落中,这样也会有助于避免这些问题。通过使用虚拟调用的途径,以便经常导致对某一特定类型的虚拟调用在大多情况下的避免对虚表的查找。通过使用部分内联来确保只对函数中经常使用的代码段进行内联,而这是有以调用函数为基础的调用来决定的。此外,某些代码段会以某种优化为前提进行编译,而另外一些代码段都按其他一些不同的目的而编译。比如说,经常使用的和/或小的函数可以按最大化性能角度来编译(/O2);而不经常使用和/或大一些的函数会被编译为占用最少的空间(/O1)。
如果你能够了解你所在的实际工作环境并能将你的应用程序投入使用,程序的性能将会得到最大限度的发挥。最近,SQL Server 使用 POGO 进行了重新编译,在很多常用情况下,获得30%的性能飙升。这样下去,你也许会猜到微软会开始使用这一技术来将它的许多产品进行重新编译。应注意的是,在你建立规范版本的配置时,不要试图覆盖全部的代码,这点是非常重要的。POGO 的全部意义在于如何优化普适性的情况。如果你对全部代码进行 POGO 优化的话,你或许会得不偿失。
Visual C++ 2005 还加入了对 OpenMP 的支持,它是一个建立多线程程序的开放性标准。它包含了一系列 Pragma 来告诉编译器哪些代码段能够进平行处理。与前面循环关系不大的一些大的循环结构最适合使用 OpenMP。看一下下面这个简单的 copy 函数,它将数组a和b的值相加,并将其和存入数组c中。
void copy (int a[],int b[],int c[],int length)
{
#pragma omp parallel
for (int i=0;i<length;i++)
{
c[i]=a[i]+b[i];
}
} 在有多处理器的机器上,编译器会生成多线程来执行每一次循环,
而每个线程将会执行复制动作的一个子集。值得注意的是,编译器无法确认这个循环是否依赖于其它代码,因此它不会阻止你在不适当的情况下使用这些 pragma。如果有依赖关系存在的话,你极有可能得到与你想像不同的错误结果,尽管这在要求上是正确的。尽管在使用 OpenMP 时最大的好处是像上面例子中的平行处理循环的情况,在直线型代码中使用它也会使性能得到改善"#pragma omp section" 参数可以用来对一段代码中非依赖性的部分做出标记,这样可以让开发人员指定可以平行运行的区域。然后编译器就能够产生多线程来在不同的处理器上执行这些部分了。
对所有使用.NET进行开发的人来说,一个重要的改变是 Visual C++2005 优化器在对 MSIL 做出的优化和对本机平台做出的优化大体上是一样的,尽管优化器是通过不同的调节选项来完成这一点的,而今天的实时(JIT)编译器是在运行时分析并优化的,它允许 C++ 编译器在初次编译时就可以进行优化,这样也可以提供极大的性能优势 ( C++编译器就有了更多的时间来进行分析而不是进行JIT ) 。Visual C++2005 编译器首次对托管类型进行优化:对循环进行的优化,表达式优化和内联。但是有些地方编译器是无法对基于.NET的代码进行优化的。举个例子来说,由于指针算术运算的问题,它就有一些无能为力了,而且某些代码由于 CLR 对严格的类型的要求和成员的可访问范围的规定而无法内联,尽管编译器的确是对合法的内联机会进行了大量的分析。此外,要优化 MSIL 需要平衡考虑对JIT 的影响。
举个例子来说,你不会想去解开一个循环而产生过多的变量给JIT编译器,因此JIT编译器必须进行寄存器分配(一个NP完成的问题) 。Visual C++ 小组正对这些问题进行研究,并在整个系统发布时会得到一个经过妥善调整的解决方案。
安全性
在 2002 年,比尔.盖茨倡导开发高信度计算机软件精神,这对微软开发的所有产品都有着不可小视的冲击力。Windows 操作系统的开发人员花费了数月的时间在安全性培训和代码讨论上,这使Windows Server 2003 成为公司曾经发布过的安全性最高的一个操作系统。微软 Office 2003 也包含了许多安全特性,像信息权利管理(IRM),更好的宏安全性和在 Outlook 中的 HTML 下载屏蔽等等。而编译器小组也大踏步的将他们开发的编译器及其产生的代码变得更为安全。
Visual Studio .NET 2002 引入了一个缓冲安全性校验的/GS编译器选项。这一标志导致编译器对那些有缓冲溢出攻击嫌疑的函数返回地址之前就预先在栈上分配空间。在进入函数之后,一个带有经过计算的值的安全性 Cookie 会被放在这个缓冲区当中,而在退出函数时,编译器会检查并保证这个 Cookie 的值没有被修改。对 Cookie 值的改变就意味着函数的返回地址有覆盖其它地址的可能性,而这会生成一个错误并导致应用程序终止。
当然,这并不能防止所有的缓冲溢出攻击。Visual Studio .NET 2003又增强了/GS这一特性。它通过整理在栈上的局部变量来使数组的内存地址高于其它的局部变量的地址,来防止这些局部变量造成溢出。这样会阻止基于虚表 vtable 劫持攻击和其它基于指针的攻击。
Figure 7 原来的/GS
Visual C++ 2005 对这一强大的特性进行了又一次的更新。当一个函数被调用时,函数的激活记录是像
Figure 7 那样排放的。如果一个局部缓冲区发生了溢出,某个攻击性程序有可能会覆盖在其栈地址上的所有东西,包括异常处理函数记录,安全性 cookie,帧指针,返回地址,和函数的参数。所有这些值都是由各种机制来保护的(像安全异常处理函数),然而,在一个以函数指针做为参数的函数中,要进行缓冲溢出仍有可能。如果一个函数将函数指针(或是一个包含函数指针的结构体或是类) 作为参数,攻击程序可能会覆盖这一指针的值并导致代码去执行任何他/她想要运行的函数。为了防止这一点,Visual C++2005 会分析所有的函数参数来查找这一弱点,并将函数激活记录像
Figure 8那样排放。那些易受攻击的参数会在局部变量的栈地址下面创建副本,而去使用这些副本而不是参数本身。一旦参数本身发生缓冲溢出,它们会因为副本的使用而仍然维持原始值,这样就不会有任何的破坏力了。
Figure 8 新的/GS
与“默认为安全的”这一高信度计算导向相一致的是,Visual C++ 2005 编译器使缓冲安全检验选项为默认激活。这会有助于使所有以 Visual C++ 编译的产品都更加安全。实际上,微软现在正在以这个激活的选项来生成它所有的产品,包括 Windows,Office 和 SQL Server。
Visual C++2005 其它的进步就是确保代码是以安全性考虑为前提生成的。绝大多数C++应用程序都是依赖于C运行库(CRT)和标准模板库(STL)的。当先对这些事情进行设计时,代码的安全性就不会被优先考虑了,而许多现在流行的攻击缺口也会被忽略。这样做的结果是,许多由这些库提供的功能大多在一个不安全的方式下被使用,使应用程序对一些潜在的攻击城门大开。最近推出的 Michael Howard 所写的《编制安全的代码》(微软出版社2002)书中,他强调了在一些情况下不使用这些库的重要性。
在Visual C++ 2005 中,微软推出了这些库的新版本,它致力于找出所有可能导致一般安全性问题的函数,并提供另外一些更为安全的版本。这一努力的长远目标是不推荐使用所有的“不安全”的版本而提倡使用一些具有相同功能且更为完善的代替版本。仅是新的 CRT 这一个运行库,就引用了超过400个新的“安全”的函数,这样会确保对所有的指针参数进行检查是否为空指针,并且所有要进行内存复制操作的函数在知道目标地址和源缓冲区之外,还要知道将会复制多少字节的数据。
结论
Visual C++2005 的新特性还有很多很多,很难在此尽述所有:相对混合镜像的延迟 CLR 载入;本机 AppDomains API;对于 AppDomain API 和进程提供更好的全局变量支持而引入的新的声明要求;模块化构造函数;对于目标文件和.NET模块提出的链接器支持;隐式装箱操作;使用和C#开发人员所喜爱的语法规则一样的XML注释;全新的面向.NET框架的STL版本;参数数组;别名提示;新的浮点数模型;操作符重载...
任何基于.NET的新版本语言经常会让人们想问“如果我们开发小组相要写一个面向.NET的应用程序,应该使用哪一种语言?”今天,如果你要做许多本机 interop 工作,那么很简单:C++是开发本机 interop 的最易用的语言,而且它常常是性能最佳的选择。此外,如果你要将现有的C++程序移植到.NET上,那就的确是没有更好的选择了。实际上,当你把现有的应用程序转化为.NET应用时,使用Visual C++ 是微软极力推荐的一条路。
至于全新的应用程序,你或许会问为什么开发人员会在.NET的世界里热衷于其中一种语言而不是另一个。由于每种语言都有其强势,对这个问题的回答不会非常简单明了,但对于纯.NET应用程序而言,在 C#,Visual Basic 和 C++ 中的体验基本相同。如果你作为一个开发人员已经习惯于使用某种特定的语言,就没有更重要的原因要转向去使用另外一种了。
但如果你在开发任何的 interop 应用时,你也许会选择 C++ 语言而不是其它。使用C++语言的经历机会肯定要好于其它的语言,因为在C++中直接内置了许多广泛的 interop 支持。此外,它所提供的在析构时的决定性清除在清理资源泄漏和确保你应用程序的正确性时简直就是无价之宝。C++还有很多强大的功能可以和 CLR 提供的功能联合使用。举个例子来说吧,C++不仅同时支持模板和泛型,而且它还支持它们的联合使用。这会比单独使用其中任何一个特色来得更有表现力,更为强大。尤其有用的一个库编写技术是写一个实现泛型接口的模板。这样会为你的模板提供所有的像特殊化这样的灵活性和强大的功能,而它仍会让其它语言有通过泛型接口而直接使用实例化模板对象这样的能力。总而言之,C++ 的确找到了属于自己的位置。
作者简介
Stephen Toub 是 MSDN 杂志的技术编辑。联系地址是:
[email protected]。
本文出自 MSDN Magazine 的 May 2004 期刊,可通过当地 报摊获得,或者最好是 订阅
本文地址:http://com.8s8s.com/it/it28120.htm