ID #1547

用Visual C++优化代码

摘要:本文介绍了Visual C++ .NET 2003产品中提供的代码优化功能。此外,对于那些还不熟悉 Visual C++ .NET 2002 中进行的改进的读者,本文还用一个较短的部分介绍了在这一改进中引入的新的“全程序优化”功能。最后,本文讨论了一些与优化有关的“最佳策略”,以及对 Visual C++ 编译器进行的一般性增强。

简介

虽然得到了一种新的工具,但对于自己是否以可能的最佳方式使用它没有把握,这总是一件令人感到沮丧的事情。该白皮书试图减少您对 Visual C++ 优化器可能具有的忧虑,从而使您确信自己正在最大限度地发挥它的作用。

Visual C++ .NET 2003

Visual C++ .NET 2003 版本增加了两个新的与性能有关的编译器选项,此外还包含了对 Visual C++ .NET 2002 中附带的几项优化的改进。

第一个新的与性能有关的选项是 /G7。该选项告诉编译器针对 Intel Pentium 4 和 AMD Athlon 处理器优化代码。

用 /G7 编译应用程序获得的性能改进各不相同,但与 Visual C++ .NET 2002 所生成的代码相比较,典型程序的执行时间减少 5% 到 10% 并不罕见,对于包含大量浮点代码的程序而言,甚至可能减少 10% 到 15%。改进的范围可能相差很大,在某些情况下,用户如果用 /G7 编译并且在最新一代处理器上运行,则会看到改进的幅度超过 20%。

使用 /G7 并不意味着编译器将产生只能在 Intel Pentium 4 和 AMD Athlon 处理器上运行的代码。用 /G7 编译的代码仍然能够在这些处理器的旧代产品中运行,尽管可能有一些小的性能损失。此外,我们已经注意到一些特殊的情况,即用 /G7 编译时产生了在 AMD Athlon 上运行速度更慢的代码。

当未指定 /Gx 选项时,编译器将默认使用 /GB,即“混合”优化模式。在 Visual C++ .NET 的 2002 版本和 2003 版本中,/GB 都等价于 /G6,也就是针对 Intel Pentium Pro、Pentium II 和 Pentium III 优化代码。

在使用 /G7 时进行的改进的一个示例是,在执行以常数为乘数的整数乘法时,更好地为 Intel Pentium 4 选择指令。例如,以下面的代码为例:

int i;
...
// Do something that assigns a value to i.
...
return i*15;

当用 /G6(默认选项)编译时,我们将产生以下指令:

mov  eax, DWORD PTR _i$[esp-4]
imul  eax, 15

当用 /G7 编译时,我们产生了更快(但更长)的指令序列,避免了使用 imul 指令,该指令在 Intel Pentium 4 上具有 14 个周期的滞后时间。

mov  ecx, DWORD PTR _i$[esp-4]
mov  eax, ecx
shl  eax, 4
sub  eax, ecx

第二个与性能相关的选项是 /arch:[参数],它采用参数 SSE 或 SSE2。该选项使编译器可以利用 Streaming SIMD Extensions (SSE) 和 Streaming SIMD Extensions 2 (SSE2) 指令,以及其他在支持 SSE 和/或 SSE2 的处理器上提供的新指令。当用 /arch:SSE 编译时,产生的代码将只能在支持 SSE 指令以及 CMOV、FCOMI、FCOMIP、FUCOMI 和 FUCOMIP 的处理器上运行。与此类似,当用 /arch:SSE2 编译时,产生的代码将只能在支持 SSE2 指令的处理器上运行。

对于 /G7 而言,用 /arch:SSE 或 /arch:SSE2 编译应用程序所获得的性能改进是不同的。通常的改进是执行时间减少 2% 到 3%,尽管在某些罕见的情况下测量到执行时间减少了5% 以上。

/arch:SSE 选项具有以下特定效果:

为单精度浮点 (float) 变量利用 SSE 指令 — 如果这样能获得性能改进。
利用 CMOV 指令 — 该指令最初是在 Intel Pentium Pro 处理器中引入的。
利用 FCOMI、FCOMIP、FUCOMI 和 FUCOMIP 指令 — 它们最初也是在 Pentium Pro 处理器中引入的。

/arch:SSE2 选项具有 /arch:SSE 选项的所有效果,并且还具有以下效果:

为双精度浮点 (float) 变量利用 SSE 指令 — 如果这样能获得性能改进。
为 64 位移位利用 SSE2 指令。

除了上述好处以外,在将 /GL(“全程序优化”)选项与 /arch:SSE 或 /arch:SSE2 结合使用来进行生成时,编译器将为具有浮点参数和返回值的函数使用自定义调用约定。

最后,在 Visual C++ .NET 2003 中,对在该产品的以前版本中引入的几项优化进行了增强。其中一项增强是能够消除传递“死”参数(那些未在被调用的函数中引用的参数)的情况。例如:

int
f1(int i, int j, int k)
{
  return i+k;
}
int
main()
{
  int n = a+b+c+d;
  m = f1(3,n,4);
  return 0;
}

在 function f1() 中,第二个参数从未使用。当用 /GL(“全程序优化”)选项编译时,编译器将为 main() 中对 f1() 的调用产生与下面类似的指令序列:

mov  eax, 4
mov  ecx, 3
call  [email protected]@[email protected]
mov  DWORD PTR [email protected]@3HA, eax

在该示例中,永远不会执行对“n”的值的计算,并且只有在 f1() 中引用的两个参数被传递给 f1()(而且它们是在寄存器中传递,而不是在堆栈上传递)。同时,该示例是在禁用内联功能的情况下编译的,因为如果启用内联功能,则该调用将被完全优化掉,而剩余的代码会将“m”设置为值 7。

Visual C++ .NET 2002

Visual C++ .NET 的 2002 版本引入了通过 /GL 编译器选项进行“全程序优化”(WPO) 的功能。其方法是将程序的中间表示形式(而不是对象代码)存储在由编译器创建的对象文件中,然后让链接器在链接过程中激活优化器和代码生成器,以便在优化时利用有关整个程序的信息。这完全是以一种非常透明的方式完成的,只涉及到对生成过程进行最低限度的更改。

WPO 的主要好处之一是能够在多个源代码模块中内联函数。这可以通过在整个可执行文件中内联小函数,大大增强内联程序改进性能的能力。WPO 的其他好处包括能够更加准确地跟踪内存别名和寄存器利用率,以便消除围绕函数调用进行的存储和重新加载操作。

以下示例显示了通过使用 WPO 而生成的代码中可能发生的一些改进:

// File 1
extern void func (int *, int *);
int g, h;
int
main()
{
  int i = 0;
  int j = 1;
  g = 5;
  h = 6;
  func(&i, &j);
  g = g + i;
  h = h + i;
  return 0;
}
// File 2
extern int g;
extern int h;
void
func(int *pi, int *pj)
{
  *pj = g;
  h = *pi;
}

如果不使用 /GL 选项编译该示例,则您将看到与为 File 1产生的以下代码类似的代码:

sub  esp, 8
lea  eax, DWORD PTR _j$[esp+8]
push  eax
lea  ecx, DWORD PTR _i$[esp+12]
push  ecx
mov  DWORD PTR _i$[esp+16], 0
mov  DWORD PTR _j$[esp+16], 1
mov  DWORD PTR [email protected]@3HA, 5
mov  DWORD PTR [email protected]@3HA, 6
call  [email protected]@[email protected]
mov  eax, DWORD PTR _i$[esp+16]
mov  edx, DWORD PTR [email protected]@3HA
mov  ecx, DWORD PTR [email protected]@3HA
add  edx, eax
add  ecx, eax
mov  DWORD PTR [email protected]@3HA, edx
mov  DWORD PTR [email protected]@3HA, ecx
xor  eax, eax
add  esp, 16
ret  0

当用 /GL 编译时,您将看到与以下代码类似的代码。请注意,已经删除了相当一部分指令。还应该注意,这一虚构示例是用 /Ob0(无内联)编译的,因为如果启用内联,则几乎整个示例都将被优化掉。

sub  esp, 8
lea  ecx, DWORD PTR _j$[esp+8]
lea  edx, DWORD PTR _i$[esp+8]
mov  DWORD PTR _i$[esp+8], 0
mov  DWORD PTR [email protected]@3HA, 5
mov  DWORD PTR [email protected]@3HA, 6
call  [email protected]@[email protected]
mov  DWORD PTR [email protected]@3HA, 5
xor  eax, eax
add  esp, 8
ret  0

除了添加 /GL 选项以外,还对优化器中的内联程序进行了增强和改进,以便当用 /Ox、/O1 或 /O2 编译时,默认行为是由编译器选择将哪些函数作为要内联的候选函数。这与以前版本的 Visual C++ 是相反的 — 在以前版本的 Visual C++ 中,只考虑标记了“inline”或“__inline”的函数。要获得以前的行为,可以将 /Ob1 选项添加到主优化选项(/Ox、/O1 或 /O2)之后。

最佳优化策略

Visual C++ 编译器包含两个主优化选项:/O1 和 /O2。/O1 的含义是“最小化大小”。它将转换为分别打开下列各个选项:

/Og — 启用全局优化
/Os — 代码小优先
/Oy — 框架指针省略
/Ob2 — 让编译器选择内联候选函数
/GF — 启用只读字符串池
/Gy — 启用函数级链接

/O2 选项意味着“最大化速度”并且类似于 /O1 选项,不同之处在于指定 /Ot(“代码速度优先”)而不是 /Os,同时还启用 /Oi(“以内联方式展开内部函数”)。

一般而言,应该用 /O2 编译小型应用程序,而应该用 /O1 编译大型应用程序,因为特大型应用程序最终可能会对处理器的指令缓存产生巨大压力,从而可能导致性能恶化。为了将这种可能性降至最低,请使用 /O1 减少编译器由于执行某些转换(如循环展开)或选择更大、更快的代码序列而引入的“代码膨胀”的数量。

通常,由 /O1 禁用的优化对于小型代码序列是有利的,但如果不受限制地在大型应用程序中使用,则可能急剧增加代码大小,并且由于指令缓存问题而足以导致总体性能下降。

在选择主优化选项之后,一个屡试不爽的好做法是分析代码以查找热点代码,然后对相应的代码试验不同的优化选项。特别地,如果您选择使用 /O1 作为所有文件的主优化选项,则一种有用的做法是微调热点函数,以便使其具有“最大化速度”的编译效果。

Visual C++ 编译器支持优化杂注,从而可以围绕特定函数微调优化控制。

例如,假设您已经用 /O1 对您的应用程序进行了编译,然后通过分析您的应用程序,发现函数 fiddle() 是该程序的“热点”函数之一。您将需要完成与下面类似的工作:

#pragma optimize("t", on)
int fiddle(S *p)
{
  ...;
}
#pragma optimize("", on)

以上代码用一对优化杂注包装该函数,第一个杂注用于将优化模式更改为“最大化速度”,第二个杂注用于还原到在命令行上指定的优化模式。

尽管 Visual C++ 没有附带分析器,但在撰写本文时,可以在以下网址找到 Compuware Corporation DevPartner Profiler Community Edition:

 

除了/O1 和 /O2 选项以外,还有一个 /Ox 选项,它的行为类似于 /O2,并且可以与 /Os 组合使用以获得类似于 /O1 的结果。建议您使用 /O1 和 /O2,而不是使用 /Ox,因为 /O1 和 /O2 提供了更多的好处。

在本文的前面讨论了/G7、/arch 和 /GL 编译器选项。

除了这些辅助性的优化选项以外,Visual C++ 还提供了以下选项:

/GA,它为 EXE 优化静态线程本地存储 (TLS) 访问。注在编译最终形式为 DLL 的代码时,不要使用该选项。
/Gr,默认情况下它启用 __fastcall 调用约定,这意味着前两个参数将在寄存器中传递(如果它们适合寄存器)。

这两个选项为代码的优化版本提供了额外的好处 — 值得尝试使用它们(如果适合)并衡量一下得到的结果。

另外一个值得考虑的开关是可以传递给链接器的 /opt:ref 选项。通过该选项可以消除未引用的函数和数据,还可以启用 /opt:icf — 它尝试将完全相同的函数(例如,您可能通过展开模板得到的那些函数)组合起来,以进一步减小代码大小。

Visual C++ .NET 中的增强编译器选项

有三个重要的编译器选项,大多数项目在用 Visual C++ .NET 2003 生成时都应该使用这些选项。这些选项中的每一个也都存在于 Visual C++ .NET 2002 中,尽管在该产品的 2003 版本中已经进行了增强。

下表简要介绍了这些编译器选项中的每一个选项的预期用法。有关详细信息,请参阅 Visual C++ 文档资料。

编译器选项 用法
/RTC1 在调试(非优化)版本中使用。插入运行时检查,以帮助您跟踪编码错误(例如使用未初始化的内存,或者混合使用 __stdcall 和 __cdecl 调用约定)。
/GS 插入检查,以便在发生改写当前执行函数的返回地址的缓冲区溢出时能够检测到。尝试阻挠恶意代码“窃取”返回地址。
注 这不是魔术弹,也不能取代良好的编码和良好的测试。您应该检查您的代码,以努力保证不会发生缓冲区溢出。
/Wp64 检测代码中存在的 64 位可移植性问题。现在使用该编译器选项可便于以后轻松地转换到 64 位系统。

小结

Visual C++ .NET 2003 包含两个新的能够增强性能的优化选项,以及对 Visual C++ .NET 2002 中包含的几项优化的改进。通过了解这些内容以及应该对 Visual C++ 所编译的每个项目使用的三个编译器选项,可以帮助您优化代码的生成。


2011-07-01 16:30
阅读:
I'm VC , Just U know Y
本站部分文章来源于互联网,版权归原作者所有。

延伸阅读:

浅谈VC++ 6.0中临时数据的存储方法

从32位应用程序到64位应用程序

使用VC++开发考场随机排座系统

VC6下用控件进行串口通信

VC++ 2008开发网络百家乐街机游戏(下)