C/C++ 开发人员采用 Windows 64 位

5. 针对 C/C++ 开发人员采用 Windows 64 位

 

发布日期: 2006-7-10 | 更新日期: 2006-7-10

5.1 开发环境

目前,Windows 64 位的开发环境由两部分组成:

在 32 位机器上开发

在 64 位机器上部署和调试

这意味着,开发周期涉及到 32 位和 64 位平台的使用。开发周期内的代码生成部分在 32 位平台上执行,而部署-调试部分则在 64 位平台上执行。

无论开发人员使用 Visual Studio 6.0 还是 Visual Studio .NET,做法都是相同的。开发人员下载并安装 Microsoft Platform SDK,然后从预先定义的开发人员环境窗口中生成一个 Visual Studio 实例。这允许 Visual Studio 链接到正确的 64 位版本库(MFC、ATL 等),并针对选定的平台进行编译。

5.1.1Visual Studio6.0

Visual Studio 6.0 可以与 Microsoft Platform SDK 配合使用来创建针对 IPF 平台的 64 位应用程序,如果应用了 SDK SP1,还可以针对 x64 平台来创建 64 位应用程序。开发代码没有限制;Visual Studio 的界面和功能保持相同。在较新的 Windows XP 64 位版本(build 1159,x64 版本)上,可以在 x64 机器的 WoW64 下原样安装并运行 Visual Studio 6。这意味着,开发人员可以在单个 x64 机器上生成、部署和调试。

5.1.2Visual Studio.NET2002 & 2003

Visual Studio .NET 2002 和 2003 可用于编译 64 位 C/C++ 代码。这两个版本都可以使用 Platform SDK 生成 x64 或 IPF 二进制文件。通过 Windows Server 2003 SP1 版本,开发人员能够使用 Visual Studio .NET 2003 在 x64 硬件上开发 x64 代码。该功能绑定到 Windows Server 2003 中,因为 Windows XP 64 位是根据 Windows Server 2003 代码基构建的,因此每个版本的 Service Pack 都是同步的。这让开发人员不再使用双平台方法来进行 Windows 64 位应用程序的开发。但是,开发人员仍需要单独开发和调试应用程序,因为 32 位 Visual Studio 不能调试 64 位代码。

5.1.3Visual Studio.NET2005

在 Visual Studio .NET 的下一个版本(名为 Visual Studio .NET 2005)中,开发人员将能够针对 64 位进行开发、部署和调试。但是,Visual Studio .NET 2005 仍然在 WoW64 模式下运行。因此,调试将远程完成(即使它能够在调试 64 位代码的机器上运行)。Visual Studio .NET 2005 允许开发人员针对其安装的模块和扩展所支持的任何 Windows 平台进行交叉编译。开发人员可以针对 32 位 Windows 以及 64 位 Windows 来构建应用程序,还可以针对移动平台来构建应用程序。


 

5.2 调试

C/C++ 开发人员有两个用于调试 64 位代码的选项:标准 Visual Studio 调试器或 WinDGB。标准 Visual Studio 调试器应该能够满足大多数需要。但是,如果开发人员需要调试内核模式代码、托管代码,或者执行较为复杂的任务,则 WinDBG 是较合适的调试器。

5.2.1Visual Studio

Visual Studio .NET 2003 中的 Visual Studio 调试器已经迁移到 64 位 IPF。它具有并提供与 32 位版本完全相同的功能。使用该调试器的好处是,大多数开发人员都很熟悉该工具,并且它是一个基于 GUI、非常直观的调试器。但它只能执行数量有限的低级别调试。

迁移后的调试器可以通过平台 SDK 使用,并且需要在本地 IPF 机器上(没有远程功能)运行。64 位版本的 Visual Studio .NET 调试器不能调试 WoW64 模式下运行的 32 位代码,也不能从运行的进程中连接和分离。

在 Visual Studio 2005 版本中,由于调试能够从构建环境中完成,因此该过程被大大简化了。Visual Studio .NET 2005 还具有类似于 WinDBG 的低级别调试的功能和特性。

5.2.2WinDBG

正如以下屏幕截图所示,WinDBG 是一个低级别调试工具。它具有一个 GUI,但不是直观的 Visual Studio GUI。这是一个强大的工具,它需要您首先花些精力来熟悉它的操作和功能。它有一个基本的 GUI,开发人员可以在其中使用鼠标、传统的点击界面和多个窗口。


 

由于 WinDBG 是一个低级别工具,因此它比 Windows 的其他调试器更加灵活。它允许开发人员追踪其部署的每种类型的代码,并且是具有这一功能的唯一一种调试器,这些类型包括:

64 位代码

在 WoW64 下运行的 32 位代码

调试 WoW64 引擎

托管代码

Windows 服务

内核模式驱动程序和代码

WinDBG 的最后一个版本允许开发人员随意地与正在运行的进程连接或分离,而不会影响进程本身。这项操作还可以远程完成。

5.3 编码问题

将 C/C++ 应用程序迁移到 64 位时,遇到的大部分编码问题可分为三类:指针转换、指针运算和对齐。在处理内联汇编程序,使用修改后的五个 API 调用之一,以及尝试跨 32/64 位边界通信时,可能会出现其他问题。

由于数据类型 Int 和 Long 的长度保持不变(仍为 32 位),因此需要修改的代码数量非常少。通常,所涉及的代码行数应该不到总代码基的 1%。这与 Unix 不同,其中 Long 要迁移到 64 位。

开发人员必须小心处理变量的对齐。未对齐对性能的影响非常严重。对于 x64,有一些性能影响,但是在 Itanium 系统上,问题更严重;异常将传播到应用程序层并会导致应用程序崩溃。

开发人员可以使用 –Wp64 编译器开关来要求编译器显示可能的移植性问题。这将使开发人员能够注意到绝大多数的移植问题。该标志在 32 位模式下同样可用。

5.3.1类型大小

从 32 位迁移到 64 位时,增长的主要类型是指针和派生数据类型,如句柄。在 Windows 64 位中,目前的指针和派生类型是 64 位 long 类型。大小增加的其他一些类型还有:WPARAM、LPARAM、LRESULT 和 SIZE_T。其中一个原因是,它们作为参数使用,并且某些函数将指针作为参数使用。

从“int”和“long”派生出的所有类型的大小仍然是 32 位,其中包括 DWORD、UINT 和 ULONG。小于 32 位的类型保留它们当前的大小。一个示例就是“short”数据类型,它仍然保留为 16 位的带符号整数。

正如前面提到的那样,Win32 API 保持不变。所进行的更改对应于五个替代函数;其中,四个由一个多态版本取代,一个用于平面滚动条:

GetClassLongPtr()

GetWindowLongPtr()

SetClassLongPtr()

SetWindowLongPtr()

这些函数的名称已经更改。此外,这些函数已经调整为使用多态数据类型(如 UINT_PTR),并使用所有更新的常量。

由于对 Win32 API 的更改极少(现在只称为“Windows API”),Win32 API 专家基本上都是 64 位 API 专家。开发人员在 Win32 技能和代码上的投入并没有损失。当然,也有少量的更改,但开发方式保持不变。

开发人员针对单个代码基的最佳做法是,同时针对 32 位和 64 位进行编译。这使得开发人员能够确保他们在 32 位代码和技能上的投入没有损失。他们不应该为了计算和操作的目的,而编写依赖或假定数据类型大小的代码。这些代码很可能无法移植,或者在移植过程中制造困难。开发人员编写的代码是否干净,一个明显的标志是:当 W4 级警告开启时,他们是否能够顺利地编译这些代码。这并不特别针对 64 位问题,许多可移植性问题都通过这个方法识别。通常,这些代码都会针对两个平台编译,并立即运行。

以下示例说明,截至目前所提到的类型大小问题。第一个示例中的指针混合使用了“int”或“long”类型。


 

在该示例中,x 是一个整数,它被赋予地址 y。这可以在 32 位上执行,因为整数和指针的大小相同,所以没有数据丢失。但在 64 位中,整数长度仍为 32 位,但指针长度是 64 位。因此,尽管在理论上该示例可以执行该赋值,但截断值会导致数据丢失。


 

第二个示例演示 int 类型在指针运算中的错误使用。将 charArray 指针转换为 int,以便计算 char 数组中的偏移值,然后再将结果转换回指针。由于数据类型的大小不同,因此会发生丢失数据的情况,内存故障也是不可避免的。


 

最后一个示例演示在应当使用 handle 类型的位置错误地使用了 LONG 类型。在 32 位环境中,将 HANDLE 转换为 LONG 始终是完全合法的,但是它不可移植。这是指针与数据类型大小不匹配导致在 64 位环境下出现问题的另一个示例。

即使指针大小加倍,由于新的多态类型,它仍然可以是透明的。它们在 32 位和 64 位中同等地表示指针。如果使用适当,它们可根据相同的源代码基在 32 位和 64 位上正确地编译和运行。如果使用不正确,指针将被截断并可能导致难以解决的错误。

5.3.2对齐问题

移植问题的另一个常见来源是数据结构对齐。数据类型倾向于根据数据类型本身的大小按边界对齐。例如,字符按 1 字节边界对齐,而整数按 4 字节边界对齐。

下图中的结构阐释了这个问题。“a”字段(是一个字符)在结构开头处正确对齐。但是,“b”和“c”整数字段在结构中的下一个可用 4 字节边界上对齐。这就在“a”和“b”之间强制使用了 3 字节填充,以便符合整数所需的 4 字节对齐。


 

同样,“d”(是一个指针)将在下一个可用的 8 字节边界上对齐。由于“a”、“b”、“c”以及“a”和“b”之间的填充总大小为 12 字节,因此需要在“c”之后添加 4 个额外的填充字节,以便“d”可以在 8 字节边界上正确对齐。

如果在运行时对结构在内存中的实现方式进行不正确的假设,那么这些结构分配上的更改会导致许多问题。

例如,假设结构中“d”的偏移值距离结构开头处 12 字节,如果尝试进行基于偏移值的直接赋值或访问,就会出现问题。目前就有现成的机制允许用户以安全且与平台无关的方式使用基于偏移值的访问。

下图说明结构在 32 位平台和 64 位平台上填充方式之间的差别。开发人员应该了解体系结构的填充规则,并在自然边界上理想地对齐所有结构成员。

下面的结构在各个平台上大不相同,因此对象跨 32/64 位边界的任何传输都会产生问题。


 

联合是可移植性缺陷的另一个来源。如果其中一个成员是指针或者其大小在 64 位中增加的任何数据类型,则联合的大小会增加。


 

联合在封闭结构内甚至内存中的对齐方式将由联合的第一个成员的大小决定。因此,如果第一个成员是指针,联合将对齐到 8 字节边界;如果是 long 类型(如示例所示),联合将对齐到 4 字节边界。

这些类型的更改会导致内存分配问题、对齐问题以及无效的偏移值运算。堆栈对齐可能也是问题的一个来源,因为在 Windows 32 位中,堆栈始终在 4 字节边界上对齐,而 Windows 64 位始终在 8 字节边界上对齐堆栈。

以下是数据在堆栈和指针赋值中的未对齐示例。对指针变量的赋值使用的内存地址不匹配 8 字节边界,因为它是从“temp”数组的第二个字符开始的。


 

在最佳情况下,诸如此类的未对齐会影响性能。代码的速度以及可执行文件的大小都会受到负面影响。这是因为编译器将添加额外的代码来修正未对齐,因而对取消引用的内存空间执行简单的读写操作都需要“先对齐后操作”这个过程。

但是,在最糟糕的情况下,会导致处理器异常和应用程序崩溃。这对 IPF 而言更是如此。以前的 x86 体系结构以无提示的方式处理这个问题,并且在遇到未对齐时不会引发异常。因为许多应用程序中没有处理这些异常的代码,所以,如果应用程序有这方面的缺陷,则很可能会失败。

违反对齐原则的结果视平台而定。包括以下情况:

x86 — 引发一个异常,但操作系统会即时修正未对齐。

IPF其行为方式类似于 x86,但操作系统不修正错误。

x64 — 硬件不引发异常;修正在硬件级别完成。

避免未对齐的方式有多种。其中一个是使用 __unaligned 关键字。它允许访问未对齐的数据;但是,即使数据对齐正确,应用程序也要付出性能代价。不推荐您使用该方法,除非其他选项均不可行。

__unaligned 关键字会导致编译器插入代码,以即时修正未对齐问题。这将增加可执行文件的整体大小,并且是性能损失的来源。

还可以使用 __declspec(align()) 指示数据应该在特定边界上对齐。此外,_aligned_malloc() 调用允许开发人员以预先对齐的方式分配内存。这是推荐的最佳做法,以确保所有数据按自然边界对齐。由于面向性能的原因,大多数应用程序供应商都迁移到 64 位平台,因此,开发人员关注对齐对于防止应用程序降级至关重要。

5.4 性能分析

性能调整应该被视为迁移到 Windows 64 位的必要阶段。由于采用 64 位的主要驱动力是性能因素,因此最佳化应用程序将产生重大的性能收益。该阶段在 Windows 64 位中十分重要,因为处理器和编译器技术已经发展得很完美并已经具有许多领先技术,所以只有采用正确的调整方法后才能利用它们。

这个过程可以分为三个阶段:第一个阶段,使用标准的独立于平台的编译器优化(例如,大小与速度),由开发人员经验驱动的基本增强功能。第二个阶段,使用芯片厂商提供的优化工具:VTune(用于 Intel)和 CodeAnalyst(用于 AMD)。这些特定于芯片的工具应该充分利用这些体系结构所提供的最新技术的优势。最后一个阶段,使用配置文件导引优化 (POGO),以利用实际数据作为指导来提高应用程序的性能。

这些阶段之间互补并且不会相互竞争。要在应用程序中获得尽可能高的性能,开发人员应该使用所有可用的工具,并确定哪些优化对其解决方案最有意义。

5.4.1编译器优化

标准 Platform SDK 已经对代码执行了众所周知的 x86 优化,例如,循环开解、内联等。如果开发人员不打算使用特定于处理器的优化或 POGO,至少应该执行这个阶段。

即使开发人员打算使用更完善的优化选项,但对于应该在何时、何地执行标准编译器选项而言,这仍然是一个进行基本分析的好方法。

5.4.2 AMDCodeAnalyst

AMD CodeAnalyst 性能分析器是一组功能强大的工具,用于在 AMD 微处理器上分析软件性能。这些工具旨在支持 x86 和 AMD64 体系结构上的 Windows 2000 和 Microsoft Windows XP 版本。

虽然大多数用户会选择 GUI,但还是要提供配置文件作为一个命令行实用工具,以加强它在批处理文件中的使用。在 AMD CodeAnalyst 性能分析器包含了 AMD Geode 处理器的“基于计时器的分析”功能。

分析在优化的第一个阶段内使用。AMD CodeAnalyst 性能分析器提供“基于计时器的分析”和“基于事件的分析”选项。

在“基于计时器的分析”中,要优化的应用程序在运行 AMD CodeAnalyst 性能分析器的机器上全速运行。EIP 采样按预定的时间间隔收集,并可以用来识别可能的瓶颈、执行损失或优化机会。

基于事件的分析使用处理器中的性能计数器来计算特定处理器事件的发生次数。当达到事件的指定计数器阈值时,抽样驱动程序会从处理器收集 EIP 采样。一个会话中最多可以分析四个处理器事件。

“基于计时器的分析”和“基于事件的分析”可以从多处理器系统的多个处理器中收集数据。

管道模拟在优化的第二个阶段内使用,以查找导致瓶颈的原因。在模拟期间,系统将首先跟踪应用程序执行,然后在选定的目标处理器上模拟。每条指令执行的详细数据都会考虑前面执行的指令以及处理器缓存的状态。模拟仅支持单个处理器执行。

“管道模拟”支持 32 位代码的模拟,并提供以下选择:AMD Athlon、Athlon XP、Opteron 和 Athlon64 处理器。“管道模拟”还支持 64 位代码的模拟,并提供 Opteron 和 Athlon64 处理器选择。

CodeAnalyst2.2功能概述

系统范围分析:CodeAnalyst 2.2 旨在分析二进制模块的性能,包括用户模式应用程序模块和内核模式驱动程序模块。

基于计时器的分析:在启用 APIC 的系统上,最高的时间精度是 0.1ms;在未启用 APIC 的系统上,时间精度是 1.0ms。

多处理器分析:CodeAnalyst 2.2 可以在多处理器系统上分析(基于计时器的分析和基于事件的分析均如此)— 最多是 8 CPU 系统。

基于事件的分析:CodeAnalyst 2.2 中基于事件的分析,旨在分析 AMD Athlon 和 AMD Athlon XP 的 32 个公共性能事件,以及 AMD Opteron 和 AMD Athlon 64 上的全部 78 个性能事件和事件组合。CodeAnalyst 中基于事件的分析,旨在同时分析最多四个事件。

执行管道模拟:CodeAnalyst 2.2 包括模拟会话的配置和执行,以及通过图形用户界面的基于计时器和基于事件的会话。

后处理:CodeAnalyst 2.2 无需模块调试信息即可显示采样分配。

5.4.3 Intel VTune

Intel VTune 性能分析器是一个有助于查找和移除性能瓶颈的工具。查找瓶颈很重要 — 只需花费少量的精力即可使应用程序的速度显著提升。难点在于查找制造瓶颈的问题。VTune 简化了这个任务并提供了查找问题的答案。它通过三个机制或技术来完成这个任务:

采样技术使用处理器中断来识别性能瓶颈,忽略系统开销。其他性能分析器(OptimizeIt、Quantify 等)不使用硬件中断,因此它们会大大降低所测试应用程序的速度。速度降低会导致应用程序无法按正常方式执行,并会导致“误检”。换句话说,就是分析器无法正确识别瓶颈。但 VTune 对硬件中断的巧妙使用降低了系统开销,从而使应用程序能够按正常方式执行,并识别真正的瓶颈。该功能还允许开发人员深入源代码,以查看特定瓶颈的位置并了解需要修正的内容。

调用图技术使应用程序二进制能够追踪程序的流控制。所收集的信息通过易于使用的 GUI 呈现给用户。用户可以识别关键路径(即,程序中的最长路径),查看每个函数所花费的时间,并了解他们需要在哪里花费精力来优化代码。

计数器监视功能使用户能够在运行时跟踪系统活动,并识别系统中的瓶颈(是内存不足,还是 I/O 性能问题,或是缓存问题)。然后,用户可以调用 Intel Tuning Assistant 来进一步了解性能问题和调整建议。

该版本中的一个新功能是按时间采样 (Sampling Over Time)。它根据时间呈现性能和处理器事件的历史。通过这个功能,用户能够查看系统行为和应用程序性能如何随着时间而改变。查看这项内容很重要,例如,可以了解特定的事件或应用程序行为模式如何影响性能。例如,如果用户希望了解特定应用程序事件(如数据库查询,或动作游戏中的爆炸)如何影响性能,则可以查看在事件发生时性能如何改变。该功能还可以帮助识别相关的性能数据,并过滤掉对解决问题无用的数据。

另一个新功能 — 选择性校准 (Selective Calibration),改进了以前版本中的校准功能。VTune 将运行一个额外的初步采样,以便为与 CPU 时钟无关的事件(即,除了 Clock ticks 和 Instructions Retired 以外的所有事件)决定适当的采样率。在以前的版本中,默认情况下,始终会为所有事件运行额外的校准,即使是那些能够根据 CPU 的时钟速度轻松并准确地计算出采样率的事件(Clock ticks 和 Instructions Retired 都带有这个额外的运行)。首次触及该功能的用户总是很困惑 — 应用程序为什么要执行两次?现在,默认事件(Clock ticks 和 Instructions Retired)就不会导致应用程序再运行两次了。

VTune Analyzer 7.1 还支持高性能计算 (HPC),机器最多使用 64 个处理器。

5.4.4 POGO

POGO 是 Windows 64 位 Platform SDK 附带的一个新工具。它是一个新的优化方法,涉及到监视应用程序的常用执行路径,以及从该信息获得潜在的优化。

POGO 的思路很简单:创建一个程序,收集有关其常见用法的统计信息,并对其进行优化,使其能够尽可能高效地执行这些任务。创建 POGO 优化的应用程序步骤如下:

将源代码编译并链接到测试代码。

在常见用法或开发人员希望为其生成优化的用法中体验测试代码。

重新编译源代码(包括在前面步骤中收集的分析信息),以便 POGO 能够生成优化的可执行文件。

以下是配置文件导引优化执行的一些较常见任务:

内联:POGO 为内联代码决定最佳时机。

功能布局:磁盘访问开销很大;该优化可以将最常用的功能放在一起。

大小/速度优化:调用较频繁的功能针对速度进行优化,调用不太频繁的功能针对大小进行优化。

块布局:优化最热门的路径,使不常用的分支需要较长的跳跃。它降低了分支的成本并增加缓存的利用率。

分离“冷”代码:将分析过程中根本不调用的代码移到末尾。因此,工作集中的页面只包含经常调用的代码。

5.6 小结和建议

上述讨论的要点包括以下几项:

即使开发人员不打算立即移植到 64 位,他们也应该尽早在 Wow64 模式下测试代码。

开发人员应该始终采用单个源代码基,并使他们生成的版本是针对每个所需的平台(x86、IPF 和 x64)。

开发人员不应该假设数据大小,并且应该谨慎访问自然边界上的数据。

开发人员应该将优化的最后一个阶段视为必要阶段,并使用所有可用的工具充分利用他们的应用程序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章