65.9K
CodeProject 正在变化。 阅读更多。
Home

Visual C++ 的积极优化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (24投票s)

2000 年 1 月 22 日

viewsIcon

275776

downloadIcon

1430

在发布版本中节省时间和空间——对抗臃肿软件!

  • 下载头文件 - 1 Kb
  • 目录

    文章

    VC6 发布后不久,我正在将一个程序重建为单独的 DLL 模块,并注意到这些发布版本 DLL 的总大小比我预期的要大得多——比原始的单个 EXE 大将近两兆字节。所以我回去检查了 Visual C++ 5.0 生成的 EXE 的输出大小与 6.0 生成的版本——即使打开了所有项目优化,6.0 版本也大得多。

    事实证明,VC5 和 VC6 都默认采用一种代码生成方法,即使你在项目设置中打开了该选项,也不会产生最小的代码。此外,VC6 和 VC7 使用与 VC5 不同的填充值——4k 对 512 字节。这会导致每个节(.data 等)四舍五入到下一个 4k 边界,从而导致过度的文件膨胀。

    VC6 中 4k 舍入的原因似乎是因为 Win98 文件调优工具——它喜欢位于 4k 边界上的程序,因为它们可以很好地契合 x86 虚拟内存页。稍后将详细介绍为什么这不是一个绝妙的主意

    我没有去摆弄一堆项目设置,而是提供了一个简洁的头文件,你可以将其包含在 Stdafx.h 中(或任何地方——这不是仅限 MFC 的东西)。它适用于 VC5 和 VC6,对 VC7(.NET)效果一般,可能也适用于 VC4——我还没有在那里试过。它仅在发布版本中生效,因此将其保留在调试版本中也是安全的。

    该头文件告诉编译器使用某些优化设置,这些设置会从源代码中移除帧指针,从而节省空间和时间。然后,它让链接器将 .data(你的文本字符串、常量、表等)、.text(你的代码所在位置)、.rdata(只读数据——常量等——参见注释)和 .reloc(重定位数据)合并成一个单元。这可以减少这些区域因舍入造成的空间浪费,在小型 DLL 和 CPL 程序中尤其明显。最后的技巧是告诉 VC6 像 VC5 一样行为,使用 512 字节的填充而不是 4k,这会进一步缩小输出。头文件注释相当详细,可以放入几乎任何程序中。我在小型和大型应用程序中都使用过它,它绝对有助于改善输出。例如,我将一个 93k 的 exe 缩小到 52k,将一个控制面板小程序从 33k 缩小到 4k(参见RRLoginV3 以获得此节省的示例)。从文件大小的角度来看,大型程序似乎受益不大,因为节省的空间与更大的文件相比很小(1600k exe 节省 40k 并不能打动多少人)。但加载时间会更快。

    使用此头文件时,请务必在发货前全面测试您的发布版本。即使没有此头文件也进行测试,以证明它不是问题的根源(我怀疑它不是——四年,十五个主要项目我使用过它,至少有 21,000 人在使用它——没有问题)。某些在调试模式下运行良好的代码在发布模式下可能会中断——这是由于 Microsoft 的优化,并且是所有平台上的所有优化器以及通用编码的一个已知问题——令人惊讶的是,我们在使用某些东西之前有多少次没有检查 null 指针或坏数据。调试模式会为我们清零,设置已知值(0xCC 等),而在发布模式下它是随机的。通常,如果你在使用此头文件时出现持续的 GPF,请停止使用或跟踪代码中导致该问题的行。

    当然,也有一些权衡。由于数据段被合并为一个,链接速度会变慢,而且总的来说,压缩文件不会比以前更好(这是因为在压缩之前删除了空白空间,而以前,空白空间被压缩隐藏了)。对我来说,这些是合理的权衡,因为我不是每天都进行发布版本构建,并且 exe 中节省的任何空间通常意味着为我的用户提供更小的下载,并且肯定会减少用户系统上使用的内存和空间!

    注释

    将 .rdata 与静态 MFC 合并几乎总是会导致更大的 EXE。当你将静态库(无论是第三方库还是你自己的内部库)与 MFC 混合使用时,无论静态链接还是非静态链接,这似乎都会影响。如果你真的想将 .rdata 段与其他段合并,请在你的项目中或在包含 AggressiveOptimize.h 头文件之前定义 _MERGE_RDATA_

    为什么不

    有人可能会说这样做是浪费时间,因为“零字节”会在 zip 文件或安装存档中被压缩掉。事实并非如此——数据是零、一还是 85858585 都没有关系——它仍然会占用空间(zip 文件中 20 字节,如果只有 *4* 个 4k 字节不相同,则为 29 字节)并需要时间来压缩和解压缩。此外,20k 的零在磁盘上 NOT 20k——这是集群冗余的大小——对于 Fat32 系统,20k 可以是 32k,对于 NTFS,如果你只多出 1 字节(向上取整),它可以是 24k。大多数最终用户没有所有有价值的开发人员(总共六个人)拥有的双 P4 Xeon 系统,配备两个 GB 的 RDram 和一个 Raid 0+1 的 Western Digital 120 兆 Special Editions,因此他们需要他们需要的任何空间和加载时间节省;在 Windows 98 上从最终用户的 64 兆内存中多占用 32k 或更多内存不是件好事。

    .NET

    使用 Visual Studio .NET(也称为 VC7)时,此头文件不像在 VC6 下那样有帮助——这是因为 Microsoft 不允许像以前那样从源代码中设置编译器选项。你必须自己添加开关——在链接器命令行选项中添加“/ignore:4078 /RELEASE /LTCG:NOSTATUS”,在 C++ 命令行选项中添加“/GL /opt:nowin98”。你还应该在 .EXE 项目的 C++ 命令行选项中添加“/GA”。这些将关闭合并警告,强制发布版本构建,并执行整个代码优化,此外还可以移除填充。/GA 选项将开启 Windows 应用程序优化——仅对 EXE 执行此操作,不要对 DLL 执行。

    VC7 生成的代码不像 VC6 和 VC5 那样紧凑。相同的代码会被填充更多,并使用更大的结构。在 Uber Box 上进行基准测试很困难,因此虽然 VC7 代码可能运行得更快,但很难证明这一点——也许在较慢的机器上,运行时间会更分散。然而,你通常需要更小的代码,以便尽可能多地放入 CPU 缓存——避免其访问较慢的内存。

    反汇编

    有趣的是,我认为这是一个额外的好处,一旦 .TEXT 段与 .DATA 段合并,代码将不再能够被 DUMPBIN(试试看!DUMPBIN /disasm filename.exe)、WinDisasm 或任何其他反汇编工具反汇编;当然,你仍然可以使用 SoftICE 之类的工具进行破解。这方面的功劳归功于 Gëzim Pani <gpani@siu.edu>,他发现了这一点并问道“为什么?”

    现在,关于解释——据我所见。代码默认情况下应该位于 .TEXT 段(为什么不是 .CODE?好问题)。/merge:.text=.data 行会导致 .TEXT 和 .DATA 段合并到 .DATA 中,如下所示(概述)

    应用程序(.EXE) 动态链接库(.DLL)
    C:>DUMPBIN Crc32.exe
        Dump of file Crc32.exe
        File Type: EXECUTABLE IMAGE
            Summary
                 2000 .data
                 1000 .rdata
                 1000 .rsrc
    C:>DUMPBIN CBase.dll 
        Dump of file CBase.dll
        File Type: DLL
            Summary
                 4000 .data
                 4000 .rdata
                 1000 .reloc
                 1000 .rsrc

    这两个文件都不会被反汇编,因为没有 .text 段。使用 DUMPBIN /header 查看 CRC32.exe 文件会显示两个重要项目:首先,入口点位于 38DF RVA(rva 是指向重定位虚拟地址的地址)。这正好位于第二个段 .DATA 中,该段从 2000 虚拟地址开始,长度为 1CE8。这意味着如果你有某个程序硬编码期望原始代码位于 .TEXT 段(例如反汇编器!),那么它将会失败。作为测试,我尝试了 Blink-Inc 的 Shrinker(这是一个运行时程序压缩器,它将你的 dll/exe 放在运行时包装器中,在内存中透明地压缩/解压缩,对程序和用户都透明;对于免费开源版本,可以尝试UPX),它运行正常。因此,除非有什么东西显式地修改 .TEXT 段,否则不会有问题。由于我知道的唯一会这样做的是代码修改工具和病毒,所以问题出现了——这是否提供任何形式的防病毒保护?或者它会适得其反,导致病毒扫描器失败?奇怪的是,Symantec 和 McAfee 都认为我们不够资格回答我们的电子邮件——McAfee 甚至要求我们先订阅他们的防病毒服务!我个人认为这不会对扫描器造成问题,因为它们正在寻找签名——文件内的模式,这与程序的构建方式无关。误报总是会发生。但是,这对于病毒可能不成立——如果它们修补了第一个 .TEXT 段,它们就会失败。但如果它们遍历头文件并修补入口点(我认为它们就是这样工作的,但现在谁知道呢),那么这对它们来说就没有区别。但由于这是纯粹的猜测(但有 20 多年的编码经验支持),直到我们从某个防病毒供应商那里得到答复,这只是一个猜测。 

    本文及源代码版权归 Todd C. Wilson (tcw@nopcode.com) © 1999-2002 所有。未经作者事先许可,不得复制本文。允许免费使用源代码,如源代码文件中所述,但不得声称为自己的作品。未经事先许可,你不得在任何其他网站或媒介上重新发布本文或随附文件;你可以参考 NOPcode.com 以了解在哪里获取它。当然,你可以在自己的项目中使用它。如果你在项目或示例代码中使用此内容,并希望广为人知,请告诉我们!

    © . All rights reserved.