对于 clang/LLVM for Windows,可通过 “类似 MSVC” 的 clang-cl 前端支持相同的开关。下面会注明 clang 工具集的一些特定开关。
C++ 语言标准 (/std)
Visual C++ 默认使用 C++14 一致性模式。若要启用 C++17 一致性模式,请使用 /std:c++17。XBOX 上的 Microsoft Game Development Kit (GDK) API 需要 C++11 或更高版本,并且游戏开发完全支持使用 C++14 或更高版本。
/std:c++latest 为 Visual Studio 2017 和 Visual Studio 2019 启用 C++20 草案一致性。由于 /std:c++latest 的行为会随 Visual C++ 服务更新而不断变化,因此应谨慎使用。
VS 2019 (16.11) 和 VS 2022 支持 /std:c++20 开关。请参阅这篇博客文章。
从 VS 2019 (16.8) 开始,Visual C++ 编译器支持有关详细信息,请参阅以下内容:/std:c11和/std::c17。但是,GameRuntime 和许多其他 GDK 标头需要 C++11 或更高版本。
- /std (指定语言标准版本) (Microsoft Docs)
- Standards version switches in the compiler (C++ 团队博客)
- C11 and C17 Standard Support Arriving in MSVC (C++ 团队博客)
使用 /O1 /Oi 进行常规优化,对 “热” 模块使用 /O2
对于 XBOX One 和 XBOX Series X/S 上的乱序执行 CPU,让代码段能够放入指令缓存通常比获得绝对最快的代码版本更重要,因此首选优化空间。仅对特定模块或函数保留使用优化时间。生成内部函数值得为游戏增加额外代码,因为它可以提高浮点性能。你还可以通过用 #pragma optimize("t", on) / #pragma optimize("", on ) 包围特定函数或一组函数,为其启用优化时间。请记住,由于激进的循环展开,优化时间 (/O2 或 /Ox) 可能会导致程序大小急剧增加。
单指令多数据 (SIMD) 自动矢量化程序优化仅在
/O2 或 /Ox 下启用。按配置文件优化 (PGO) 可以更好地选择究竟在何处应用优化时间与优化空间。出于历史原因,需要注意的是,/Ox 与 O2 几乎相同,但缺少 /GF (消除重复字符串) 和 /Gy (启用函数级链接)。因此,通常应优先使用 /O2 而不是 /Ox。如果使用 /Ox,请务必至少显式启用 /Gy,这对于启用链接器优化非常重要。/fp:fast 编译器开关
Visual Studio 2005 引入的 /fp 标志控制编译器使用的各种浮点模型 (fast、precise、strict、except)。快速浮点模型使用许多不属于核心 “C” 规则的优化。精确浮点模型会禁用这些额外优化,并且是默认模型。我们建议对游戏使用 /fp:fast。
有关详细信息,请参阅以下内容:
- /fp (指定浮点行为) (Microsoft Docs)
- Do You Prefer Fast or Precise? (C++ 团队博客)
/arch 编译器开关
x64 本机平台的隐含默认值为 /arch:SSE2。对于 XBOX One 主机 (AMD Jaguar CPU),应使用 /arch:AVX。对于 XBOX Series X/S (AMD Hercules CPU),我们建议使用 /arch:AVX2。
使用 /arch:AVX 或 /arch:AVX2 的主要好处是,这会使编译器生成 SSE 指令的 VEX 前缀版本,这些版本使用非破坏性的三操作数编码,而不是标准的双操作数编码 (其中一个源操作数会被结果覆盖)。这既影响使用 SSE 指令的 x64 本机代码的标量浮点运算,也影响 SSE 内部函数的显式使用。这有助于降低寄存器调度压力,并可以消除一些 movaps 指令。
有关详细信息,请参阅以下内容:
- /arch (x64) (Microsoft Docs)
- AVX2 floating point improvements in Visual Studio 2019 version 16.5 (C++ 团队博客)
/d2vzeroupper,以针对 AMD Hercules CPU 在 vzeroupper 指令使用方面的首选行为进行优化。对于全程序优化 (/LTCG),所需的等效链接器开关为 /d2:-vzeroupper。这是 VS 2022 编译器的默认行为。
对于使用 Visual Studio 2022 的 XBOX One,还应使用 /d2vzeroupper-,以针对 AMD Jaguar CPU 在 vzeroupper 指令使用方面的首选行为进行优化。对于全程序优化 (/LTCG),所需的等效链接器开关为 /d2:-vzeroupper-。这是 VS 2019 编译器的默认行为。
对于 clang/LLVM,XBOX One (AMD Jaguar CPU) 使用-march=btver2,XBOX Series X/S (AMD Hercules CPU) 使用-march=znver2。
对于 PC 开发,可以确信所有 x64 本机 CPU 都支持 SSE 和 SSE2,因为这是体系结构定义的一部分。你也可以放心地要求 SSE3,因为 Windows 10 或 Windows 11 支持的所有现代 Intel CPU 都支持它。对于在 PC 上使用的任何超出 SSE2 的指令集 (例如 AVX 或 AVX2),请务必在运行时检查 CPU 支持情况。
/favor 编译器开关
/favor 开关控制 x64 编译器针对 AMD64 与 Intel 64 处理器的优化选择。对于 XBOX 开发,请使用 /favor:AMD64。
异常处理模型 (/EHsc)
对于 x64 本机代码,异常处理不会对函数尾声或序言产生任何直接的代码影响。Windows x86 使用基于堆栈的方案,需要向模块中注入额外代码,而 x64 本机代码生成则使用基于表的方案,以确保在异常处理期间可以正确展开堆栈。x64 本机使用的基于表的方案还有一个优点是更安全,因为它不受堆栈缓冲区溢出攻击的影响。这种设计带来了许多影响。
- 异常处理不会影响生成代码中的指令数。像 Windows x86 使用的基于堆栈的方案可能导致 5-10% 的代码开销,而 x64 本机异常处理仅生成与可执行文件关联的基于表的数据,而不会生成更多指令。即使为 x64 本机显式禁用异常处理 (我们不建议这样做),也只能为 C++ 处理节省少量辅助数据,并且不会禁止生成堆栈展开数据。
- try/catch 块的使用成本很低。在基于堆栈的方案中,try/catch 块会添加一些用于跟踪的额外代码。在 x64 上,这只会在展开表中添加更多条目。
- 异常处理 (即实际引发异常) 需要额外进行一些表查找。其中的权衡是,虽然基于堆栈的方案可以更快地引发异常,但这样做会使所有代码路径都承受开销。基于表的方案 “引发” 较慢,但不会影响其他代码路径。由于异常不应用作正常的流控制,实际上只应在异常情况下触发,因此与应用程序的整体性能相比,这种额外的处理性能成本可以忽略不计。
HRESULT 而不是异常处理来返回错误结果,因此在游戏代码库中采用 C++ 异常处理的程度完全由你决定。异常不应用于流控制,因此通常最好用于 “快速失败” 方案,这可以简化调试和编码。许多标准 C++ 库都假定已启用异常处理,以便正确处理错误。
不建议显式禁用异常处理,因为这会对崩溃后分析产生负面影响,并且不会改进代码生成。我们不建议使用 /EHa,因为它可能会抑制优化器。
运算符
new 在分配失败时始终会引发 C++ 异常。如果要使用不引发异常的 new 版本,必须使用 new (std::nothrow)。有关详细信息,请参阅 std::nothrow (cppreference.com)。std::unique_ptr、std::shared_ptr 和 std::lock_guard。使用 Microsoft::WRL::ComPtr 等 COM 智能指针自动处理 COM 引用计数是 “异常安全” 的,并且不容易出错。通常,应优先使用资源获取即初始化 (RAII) 编码模式。
默认情况下,使用 VS 2019 生成的 x64 本机代码使用改进的异常处理程序实现 (
__CxxFrameHandler4、__GSHandlerCheck_EH4)。因此,使用 VS 2019 生成的静态库与 VS 2017 C/C++ 运行时不兼容。使用 VS 2017 生成的静态库与 VS 2019 兼容。- /EH (异常处理模型) (Microsoft Docs)
- x64 异常处理 (Microsoft Docs)
- Making C++ Exception Handling Smaller On x64 (C++ 团队博客)
运行时库 (/MD、/MDd)
建议选择使用 Visual C/C++ 运行时的多线程 DLL 版本。这可以最大程度地减少代码膨胀,避免 DLL 之间交互可能出现的问题,简化服务,并且是所有 Microsoft 平台的推荐选项。
对于 XBOX 上的 Microsoft Game Development Kit (GDK),这意味着链接到 ucrt.lib 或 ucrtd.lib。
从技术上讲,你可以通过
/MT、/MTd 使用 libucrt.lib 或 libucrtd.lib 进行静态 CRT 链接。GDK 不包含与静态 CRT 兼容的扩展库,因此对此方案的支持有限。调用约定 (/Gd、/Gv)
x64 的本机代码生成使用简单且标准的 __fastcall 调用约定,该约定可以在寄存器中传递前四个参数。
- 如果第一个参数是整数或指针等效项,则在 RCX 中传递。如果是
float或double,则在 XMM0L 中传递。否则,通过堆栈传递。 - 如果第二个参数是整数或指针等效项,则在 RDX 中传递。如果是
float或double,则在 XMM1L 中传递。否则,通过堆栈传递。 - 如果第三个参数是整数或指针等效项,则在 R8 中传递。如果是
float或double,则在 XMM2L 中传递。否则,通过堆栈传递。 - 如果第四个参数是整数或指针等效项,则在 R9 中传递。如果是
float或double,则在 XMM3L 中传递。否则,通过堆栈传递。 - 所有其他参数都通过堆栈传递。
- 适合 64 位的返回值在 RAX 中返回。XMM0 用于返回
float、double、__m128、__m128i和__m128d值。否则,调用方会在 RCX 中将指向内存的指针作为第 0 个参数传入,并且 RAX 必须返回同一指针。
__fastcall 方案中,SIMD 值永远不会在寄存器中传递。
对于 x64 本机代码,
__cdecl、__stdcall 和 __thiscall 都是 __fastcall 的别名。矢量调用
Visual Studio 支持一种可选的调用约定,称为__vectorcall,用于 x64 本机代码生成。
- 如果第一个参数是整数或指针等效项,则在 RCX 中传递。如果是
float、double、__m128、__m128i或__m128d,则该参数在 XMM0 中传递。如果是__m256、__m256i或__m256d,则在 YMM0 中传递。否则,第一个参数通过堆栈传递。 - 如果第二个参数是整数或指针等效项,则在 RDX 中传递。如果是
float、double、__m128、__m128i、__m128d、__m256、__m256i或__m256d,则该参数在 XMM1/YMM1 中传递。否则,第二个参数通过堆栈传递。 - 如果第三个参数是整数或指针等效项,则在 R8 中传递。如果是
float、double、__m128、__m128i、__m128d、__m256、__m256i或__m256d,则该参数在 XMM2/YMM2 中传递。否则,第三个参数通过堆栈传递。 - 如果第四个参数是整数或指针等效项,则在 R9 中传递。如果是
float、double、__m128、__m128i、__m128d、__m256、__m256i或__m256d,则该参数在 XMM3/YMM3 中传递。否则,第四个参数通过堆栈传递。 - 如果第五个参数是
float、double、__m128、__m128i、__m128d、__m256、__m256i或__m256d,则该参数在 XMM4/YMM4 中传递。否则,第五个参数通过堆栈传递。 - 如果第六个参数是
float、double、__m128、__m128i、__m128d、__m256、__m256i或__m256d,则该参数在 XMM5/YMM5 中传递。否则,第六个参数通过堆栈传递。 - 所有其他参数都通过堆栈传递。
- 适合 64 位的返回值在 RAX 中返回。XMM0 用于返回
float、double、__m128、__m128i和__m128d值。YMM0 用于返回__m256、__m256i或__m256d值。否则,调用方会在 RCX 中将指向内存的指针作为第 0 个参数传入,并且 RAX 必须返回同一指针。
__vectorcall 约定还可以处理同质矢量聚合 (HVA) 类型,这类类型由一个或多个相同类型的矢量或浮点类型组成,最多四个成员 (即 struct * { __m128 v[4]; } 是 HVA)。
作为按值传递的参数,如果在分配其他参数后 XMM0/YMM0 - XMM5/YMM5 中有足够的空间,HVA 将在寄存器中传递。作为返回值,HVA 通过 XMM0/YMM0 - XMM3/YMM3 返回。
__vectorcall 不支持 vararg 函数或 C++ 构造函数。- __vectorcall (Microsoft Docs)
- Introducing ‘Vector Calling Convention’ (C++ 团队博客)
全程序优化 (/GL、/Gw、/LTCG)
传统的编译器优化技术可以优化函数和模块,而全程序优化 (也称为链接时代码生成 (LTCG)) 则跨模块执行优化。这包括在更好地了解副作用的基础上改进寄存器分配和调度、过程间常量/范围传播、部分内联,以及尽可能对对象上的函数调用进行去虚拟化。
LTCG 还可以在增量模式下使用,以便在开发迭代中获得代码性能优势。有关详细信息,请参阅 Speeding up the Incremental Developer Build Scenario (C++ 团队博客)。
对于提供预生成静态 C++ 库的中间件供应商,启用 WPO 会将库绑定到编译器工具集的特定版本。在这种情况下,应提供不使用 WPO 的库。此建议不适用于 DLL 或从源代码生成的代码。有关详细信息,请参阅 /GL (全程序优化) (Microsoft Docs)。
Visual Studio 2022 (17.5) 添加了一个与/Gw相关的新一致性开关/Zc:checkGwOdr。请参阅这篇博客文章。
按配置文件优化
按配置文件优化 (PGO) 将 LTCG 过程更进一步,全程序优化由先前运行应用程序时收集的性能跟踪来控制。这可以提高现有优化的质量,并改进函数布局、块布局和代码分离方面的优化,从而改善工作集和分支预测。 有关详细信息,请参阅按配置文件优化 (Microsoft Docs)。/OPT:REF,ICF 链接器开关
这些开关会在将各种对象模块链接到最终可执行文件时消除其中的冗余内容。通常,调试版本会禁用这些开关,但发布版本中应启用它们。
为了获得最佳效果,请务必使用
/Gy (启用函数级链接) 选项进行编译。/GS 编译器开关
我们建议在发布配置中对所有代码使用缓冲区检查。如果特定缓冲区不受安全威胁,但检查似乎影响了性能,请在函数上使用 __declspec(safebuffers),而不是全局禁用 /GS。对于风险较高的代码,请使用 #pragma strict_gs_check(on)。
有关详细信息,请参阅 /GS (缓冲区安全检查) (Microsoft Docs)。
/homeparams 编译器开关
在优化版本中使用此开关可以大大提高调试应用程序发布版本的能力。如果用于正式版本,它可以简化分析和调试故障转储的过程。这会因在堆栈上存储额外参数而增加少量开销。在大多数情况下,这不会对性能产生明显影响。
有关详细信息,请参阅 /homeparams (将寄存器参数复制到堆栈) (Microsoft Docs)。
/Zc:inline 编译器开关
此开关会对内联函数声明应用更严格的 C++11 规则,该规则要求所有内联函数的主体与其声明和使用位于同一编译单元中。使用此标志可以缩短生成时间,并显著减小 .obj 文件的大小。如果内联函数不符合 C++11,则会在链接时收到 LNK2019 错误。使用 MSBuild 生成时,此开关默认处于启用状态,但使用命令行、CMake 等生成时必须显式添加。
有关详细信息,请参阅 /Zc:inline (删除未引用的 COMDAT) (Microsoft Docs)。
/Zc:throwingNew 编译器开关
C++ 标准和标准 C++ 库模板要求运算符 new 在分配失败时引发异常,而不是返回 nullptr;你可以将 std::nothrow 与 new 一起使用,以强制返回 nullptr 的行为。长期以来,Visual C++ 编译器在每次调用 new 之后都会生成 null 检查,而在存在异常处理的情况下,这是不必要的。使用此开关可以消除这些额外的检查。
有关详细信息,请参阅以下内容:
- /Zc:throwingNew (假定运算符 new 引发) (Microsoft Docs)
- New in VS 2015: /Zc:throwingNew (C++ 团队博客)
/Zc:__cplusplus 编译器开关
出于历史原因,Visual C++ 中的 __cplusplus 宏在很长一段时间内都返回 “199711”。从 Visual Studio 2017 (15.7 更新) 开始,这个新开关会使 Visual C++ 编译器返回正确的标准值。编译器的未来版本将默认执行此操作,因此值得尽早启用它,以解决任何遗留的一致性问题。
有关详细信息,请参阅以下内容:
- /Zc:__cplusplus (启用更新的 __cplusplus 宏) (Microsoft Docs)
- MSVC now correctly reports __cplusplus (C++ 团队博客)
/volatile:iso 编译器开关
关键字 volatile 具有许多与其行为相关的 Microsoft 特定扩展行为,这些行为并不真正可移植,即使在不同代的 Visual C++ 之间也是如此。应考虑启用此开关,然后确保代码中没有误用 volatile。
有关详细信息,请参阅 /volatile (volatile 关键字解释) (Microsoft Docs)。
/permissive- 编译器开关
Visual Studio 2017 引入了一个新的标准一致性开关,以替换过时的 /Za 开关。使用 Microsoft 特定扩展行为时,此开关会使编译器发出错误和警告,因为这些行为在移植到其他编译器时可能会导致问题。使用 /permissive- 还可以缩短编译时间。
有关详细信息,请参阅以下内容:
- /permissive- (标准一致性) (Microsoft Docs)
- Give Visual C++ a Switch to Standard Conformance (C++ 团队博客)
从 Visual Studio 2017 (15.3 更新) 开始,使用
/permissive- 还会启用两阶段名称查找。某些 C++ 模板代码在正确执行标准时无法编译,因此你可以使用 /Zc:twoPhase- 有选择地仅禁用该部分,而不是完全禁用 /permissive-。有关详细信息,请参阅以下内容:- /Zc:twoPhase- (禁用两阶段名称查找) (Microsoft Docs)
- Two-phase name lookup support comes to MSVC (C++ 团队博客)
/Zc:preprocessor 编译器开关
Visual Studio 2019 (16.6) 包含一个用于启用符合 C99 的预处理器的新开关。此开关默认处于关闭状态,但如果关注代码可移植性,应考虑启用它。请参阅 Announcing full support for a C/C++ conformant preprocessor in MSVC。
/ZH:SHA_256 编译器开关
调试符号传统上使用 MD5 哈希。为了提高供应链安全性,建议改用 SHA-256。请注意,Visual Studio 2022 默认使用 /ZH:SHA_256。
clang/LLVM 从 v16 开始支持此开关。
/NXCOMPAT 和 /SAFESEH 链接器开关
使用 x64 本机目标时,这两个开关是隐式的。XBOX 游戏需要不执行 (NX) 保护,因此不能使用 /NXCOMPAT:NO。
/DYNAMICBASE 链接器开关
此开关启用地址空间布局随机化 (ASLR),并且默认处于启用状态。请注意,XBOX 游戏需要此功能,因此不能使用 /DYNAMICBASE:NO。
/Debug:FASTLINK 链接器开关
对于 Visual Studio 2019 (16.8) 或更高版本,由于链接器的性能显著提高,我们不再建议使用 /Debug:FASTLINK。
请记住,使用
/Debug:FASTLINK 时,项目的主 pdb 仅包含指向分散在整个生成中的 pdb 文件的链接。如果要共享或存储 /Debug:FASTLINK 生成中的符号,可以使用 mspdbcmf.exe 命令行工具生成完整的 pdb 文件。有关详细信息,请参阅以下内容:- /DEBUG (生成调试信息) (Microsoft Docs)
- Faster C++ build cycle in VS 2017 with /Debug:fastlink (C++ 团队博客)
库
对于 Gaming.Xbox.XboxOne.x64 和 Gaming.Xbox.Scarlett.x64 平台,必须链接xgameplatform.lib。这是一个 “伞” 库,包含主机上支持的所有 Win32 API。不要链接 kernel32.lib、onecore.lib、onecoreuap.lib 或 WindowsApps.lib。不建议混合使用伞库,否则可能导致进程启动失败。
有关 “伞” 库的详细信息,请参阅 Microsoft Docs。
/NODEFAULTLIB:library 链接器开关
我们建议将 /NODEFAULTLIB 链接器开关与特定的常用库名称列表一起使用,以确保在链接时不会通过 #pragma comment(lib,) 使用不受支持的库。
如果某个库被显式列为链接输入,那么即使它被列为
/NODEFAULTLIB 库,也仍会被使用。此开关只忽略隐式引用。Visual C++ 运行时库
Visual Studio 开发人员命令提示 定义了查找这些路径所需的环境变量。 供 Gaming.Xbox.*.x64 查找 Visual C++ 运行时导入库的正确/LIBPATH 是 %VCToolsInstallDir%lib\onecore\x64。
动态链接库 (DLL)
Gaming.Xbox.*.x64 的 Game OS 不包含 Visual C++ 运行时 DLL 库。这些库包含在游戏布局中,以便游戏能够启动。-
对于发布版本,你需要
%VCToolsRedistDir%onecore\x64\Microsoft.VC14?.CRT的内容。如果同一布局中混合了调试和发布二进制文件,则需要在所有布局中包含这些内容。 -
对于调试版本,你需要
%VCToolsRedistDir%onecore\debug_nonredist\x64\Microsoft.VC14?.DebugCRT的内容。 -
对于调试版本,你还需要
ucrtbased.dll的调试版本,该文件位于 Windows SDK 中:%WindowsSdkVerBinPath%x64\ucrt。ucrtbase.dll的发布版本包含在 Game OS 中。
如果使用 OpenMP,还需要
Microsoft.VC14?.OpenMP 和/或 Microsoft.VC14?.DebugOpenMP 的内容