较短的构建时间可以提高开发人员的生产力。因此,如果可能的话,减少它们是有益的。本文档解释了为什么编译可能很慢,并讨论了加快编译速度的不同方法。
这主要是为了成为其他正在调查编译时间问题的开发人员的资源。然而、如果能听到其他人对我们如何能够或应该改善建造时间的想法,那就太好了。
构建过程
在分析编译时间时,构建过程的三个部分特别有趣。这些在下面简要描述。
源代码解析
这通常被称为编译器前端。首先,它为每个翻译单元运行预处理器。这些内联都包含头文件,通常导致一个相当大的文件(通常>100,000行)。
预处理器本身非常快,但解析许多行代码的速度很慢。解析C++代码比解析C代码慢,因为C++是一种更复杂的语言。在解析结束时,编译器具有当前翻译单元和所有包含的标头的内部表示(例如AST)。
代码生成
现在,编译器检查必须生成哪些函数,因为它们可能被其他翻译单元调用。然后,它为这些函数生成机器代码,这也涉及处理同一翻译单元中间接调用的所有其他函数。这通常被称为编译器后端。
这里的编译时间主要取决于生成的机器代码量(而不是预处理文件的大小)。在C++中,这通常会导致生成许多(模板)内联函数。在C代码中,其中许多函数不会是inline
,这使得在相同数量的代码下,C代码生成通常更快。在C++中,由于模板机制,它们通常必须是inline
。
链接
链接器读取每个翻译单元的符号表,即翻译单元使用和提供的函数和变量列表。然后,它将每个使用过的函数复制到最终的可执行函数中,并更新函数指针。
链接时间主要取决于链接器读取和删除所有符号表的速度,以及复制和链接最终可执行文件中所有功能的速度。
翻译单元越多,相应的机器代码大小和符号表越大,链接过程就越慢。
冗余工作
构建大型C++项目缓慢的主要原因是编译器和链接器会做大量的冗余工作。相同的标头被一遍又一遍地解析,并多次生成相同的(模板)内联函数。这导致生成的对象文件中出现许多重复函数,链接器的工作是再次减少冗余。冗余工作通常比每个翻译单元独特的工作要多得多。
构建缩短时间的方法
有不同的技术可以缩短建造时间。一般来说,他们通过减少冗余工作、减少独特工作或更快地完成相同的工作来工作。减少独特的工作通常涉及运行时性能权衡。
更快的链接器
链接是构建过程的最后一步,即使只更改了单个源文件,也必须完成。通常,每次更改后,它都必须从头开始重新链接所有内容,除非使用增量链接之类的东西(我在Blender中没有这方面的的经验)。
许多链接器大多是单线程的,因此在构建过程结束时是一个巨大的串行瓶颈。
如果可以的话,用模具 88链接器。我已经使用它很长时间了,它比我尝试过的所有其他东西都要快得多。人们报告了它做错事的问题,但我从未经历过这些。不幸的是,它还没有在Windows上可用。macos上的Xcode 15中的新链接器似乎也比以前的版本快得多。
缓存
构建系统通常根据文件系统上的最后一次修改时间决定重建文件。当人们经常更新上次修改时间而不实际更改源代码时,这就会成为一个问题。缓存 20包装另一个编译器,如gcc,并在其上添加一个缓存层。它首先检查要编译的源代码是否实际更改。如果是这样,则调用底层编译器。否则,它只是输出之前编译器调用的结果。
这个缓存层在某些情况下会有所帮助。显然,当只是在不进行任何更改的情况下保存现有文件时,它避免了重新编译。仅仅更改注释时也有帮助,因为Ccache可以检查预处理器输出是否已更改,预处理器是否删除了注释。最重要的是,它在分支之间来回切换时有帮助。切换到之前已编译的分支后重新编译,使用缓存,因此速度要快得多。
请注意,ccache仅缓存代码生成步骤的输出,而不缓存链接器结果。因此,即使源文件没有更改,链接器仍然必须再次运行。
当对编译时间进行基准测试时,Ccache可能会碍事,因为它使结果不可靠。通过将CCACHE_DISABLE
环境变量设置为1(export CCACHE_DISABLE=1
),可以暂时禁用Ccache。
多个git checkout
拥有多个git checkout 可以通过避免经常导致许多代码更改的分支切换来减少编译时间。可以简单地复制源代码文件夹或使用git工作树。每个结账都应该有自己的构建文件夹。
就我个人而言,我有一个单独的检查,我不用于编译,只是在处理某些事情时查看其他分支,或检查非常旧的提交,例如,当弄清楚何时添加了特定函数时。
当处理多个结账时,很容易将自己与您当前正在处理的结账搞混淆。我通过使用vscode为每个文件夹配置不同主题的能力,在本地解决了这个问题。从本质上讲,当我不在主结账时,vscode看起来就不一样了。自从我设置这个以来,我从未意外更改过错误的结账。
多个构建文件夹
为不同的配置拥有多个构建文件夹可以减少在它们之间切换时的编译开销。例如,应该有单独的构建文件夹用于发布和调试构建。部分,这是在使用我们的make
实用程序时自动设置的。Visual Studio还会自动创建单独的构建文件夹。
不过,拥有更独立的构建文件夹可能是有益的。如前所述,为每个git checkout提供单独的构建文件夹会有所帮助。就我个人而言,我目前有以下配置的构建文件夹:release
、debug
、reldebug
、asan
、compile commands
(仅用于在vscode中更好的自动完成)、clang release
(用于检查其他警告和性能差异)、clang release optimized
(启用额外的编译器优化以查看它们是否有影响)、release reference
(用于比较两个分支的性能)。
类型的前向声明
前向声明允许使用来自一个标头的类型,而实际上不包括标头。例如,将struct bNodeTree;
放在标头中,使bNodeTree
类型可用,而无需包含DNA_node_types.h
。希望至少一些使用正向声明的翻译单元无论如何都不包括DNA_node_types.h
。这导致预处理翻译单元中的代码更少,从而减少了解析开销。
最终包含在大多数翻译单元中的转发声明类型实际上不会为编译时间提供任何好处。例如,转发像Vector
这样的常见容器很少有意义。
仅与正向声明类型一起工作的标头受限于它们对该类型可以执行什么操作。他们无法访问其任何成员。这包括间接访问,例如通过按值获取或返回类型。通常,只能使用该类型的指针和引用。支持智能指针,只要它们不必访问该类型的方法(如析构函数)。如果标头中只有一个函数需要访问成员,那么无论如何,包含最终都是必要的。
转发声明某些类型比其他类型更难。C结构通常易于转发声明。嵌套的C++结构无法转发声明。命名空间和模板也使转发声明更难。例如,人们无法真正在标准库之外可靠地转发声明std::string
。那是因为它实际上是另一种类型的typedef。一些标准类型在#include <iosfwd>
中有前向声明,但不是全部。该标头的存在还表明,转发声明这些类型并非微不足道。也看看这个 12。
在一些标头中,我们目前访问结构的数据成员,即使它们是向前声明的。这之所以有效,是因为数据成员用于预处理器宏。例如IDP_Int
。当我们想用内联函数替换宏时,这是一个问题,这需要包含。
在C++代码中,看到转发声明的好处通常很困难,因为代码更改通常只对编译时间产生不可忽视的影响,因此很难衡量整体有效性。这也是因为翻译单元最终往往非常大,以至于我们自己的一个标题不会有太大区别。另一个问题是,很难始终如一地使用前向声明,特别是在C++中,因为标头中定义的许多东西都需要完整的类型。很难在任何地方使用前向声明,但以后在必要时很容易引入包含,使之前的工作变得毫无用处。
较小的Header
这个想法是将大Header拆分成多个较小的Header。希望以前包含大Header的一些翻译单元现在只包含较小Header的子集。这可以减少解析时间。
当Header的一部分需要另一个非常大的Header时,这可能特别有益。例如,BKE_volume_openvdb.hh包括openvdb,并非所有处理卷数据块的代码都使用openvdb。
较小的Header也有助于使代码更容易理解。无论如何,许多大Header已经有了部分。将部分拆分成单独的Header可能是一个很好的清理。只有当单个标头没有像inEDED_asset.h
再次分组到一个更大的标头中时,这才会改善编译时间。
类型擦除
模板通常必须在标题中定义,以便每个翻译单元都可以实例化其所需的版本。从函数中删除模板参数允许在标头之外定义它,从而减少解析和代码生成的开销。
显然,许多功能都是模板是有充分理由的,但在某些情况下,可以在不牺牲性能的情况下使用类型擦除。例如,当前将可调用的函数作为模板也可以取FunctionRef
。函数使用Span<T>
可以使用GSpan
。
有时,正常和类型擦除的代码路径的组合也很有意义。例如,parallel_for
有一个内联快速路径,但一个类型删除了不在标题中的更复杂的路径。在某些情况下,类型擦除也可以[显著]减少最终的二进制大小。
显式实例化
有时,不可能或不希望从函数中删除模板参数。然而,如果该函数因为太大而无法真正从内联中受益,那么在使用它的每个翻译单元中编译它就没有意义了。仅编译一次可以减少代码生成时间。
显式实例化可用于确保模板函数仅针对特定类型实例化一次。例如,请参阅 normalized_to_eul2 函数。
这种方法还可用于将模板的定义移出标题,在这种情况下,这也将减少解析开销。然后,模板函数只能与已显式实例化的类型一起使用。
虽然显式实例化肯定会有所帮助,但找到具有可衡量影响的优秀候选人并非完全微不足道。也许一些更好的工具可以在这里有所帮助。
重用常用功能
当标头已经包含将在源文件中实现的功能时,最好只使用共享功能。这可能看起来很明显,但这里提到它,因为它不一定简单,需要熟悉可用的实用程序。例如,许多几何算法都有一个步骤。
拥有更常见的功能,而不是每次使用都会重新编译,从而减少代码生成冗余。不过,拥有太多很少使用的此类实用程序也会增加解析开销。
使用较少的强制内联
编译器相当擅长决定哪些函数应该内联。它使用各种启发式方法来实现运行时性能和二进制大小的良好平衡。一般来说,编译器会考虑在翻译单元中定义的所有函数进行内联。使用inline
关键字大多只是链接器的信息,告诉它,在不同的翻译单元中,一个函数可能有多个定义,并且它们应该被重复数据删除。也就是说,根据编译器的不同,inline
关键字可能会改变启发式,使内联的可能性更大。
编译器还支持强制内联,例如使用__forceinline
。现在,编译器通常只是跳过启发式,并内联所有内容。这通常是期望的行为,但如果使用得太慷慨,可能会产生不良后果。它可能导致编译缓慢的巨大函数。编译函数通常具有比线性慢的时间复杂度。也看看(https://aras-p.info/blog/2017/10/09/Forced-Inlining-Might-Be-Slow/)。
最好只使用普通的inline
函数,只有当您注意到编译器没有内联时才使用强制内联,以便在内联时导致更好的性能。
预编译的Header
解析代码的大部分时间来自解析最常用的头文件。预编译的标头允许编译器对一组标头文件进行一些预处理,这样它们就不必为每个翻译单元进行解析。使用预编译的标头大多可以通过cmake实现自动化。
虽然预编译的标头可以减少大量冗余的解析时间,但它们不会减少代码生成步骤中的冗余。
当使用预编译的标头时,人们可能会意外地创建无法再自行编译的源文件。这是因为当模块中使用预编译的标头时,所有翻译单元都包含它。修复相关问题通常只是意味着添加更多包含语句。只需暂时禁用预编译的标头,就很容易自动找到这些问题。
当只有少数翻译单元使用预编译的标头时,使用它们也会损害性能。这是因为预编译标头是单线程的,只有当标头编译完成时,翻译单元的编译才会开始。当使用单位大小相对较大的统一构建时,这种效果最明显。
统一构建
解析和代码生成步骤中的冗余性很糟糕,因为与每个翻译单元独有的非冗余工作相比较长。Unity将多个源文件串联到单个翻译单元。现在,所有标头都必须解析一次,内联函数的代码必须每10个源文件生成一次。因此,相对于独特的工作,花在做冗余工作上的时间大大减少了,在这种情况下减少了10倍。
Unity构建可以与预编译的标头一起使用。这可以帮助具有大量源文件的模块在解析标头时消除剩余的冗余。总体而言,预编译的标头在统一构建中变得不那么有效,因为解析开销已经大大减少。
与预编译的标头类似,使用统一构建可能会导致意外拥有无法再自行编译的文件。这通常也通过添加缺失的包含来修复。
Unity构建通常需要一些工作才能工作和安全。根本问题是,以前是单个源文件本地的符号,现在在同一单元的其他源文件中也可用。这可能会导致名称冲突,从而破坏编译。因此,人们必须小心使所有源文件彼此兼容。通过将所有源文件放入一个单元,是否可以成功测试一个准备好的文件。
只是希望没有名称冲突通常是不够的,至少当任何局部变量突然与另一个文件中的局部变量发生冲突时,它不会让人安心。一个能让人更安心的解决方案是将每个源文件放入自己的命名空间。只有显式公开的符号才在该命名空间中定义。我们已经在大多数节点实现中使用了这种方法。例如,请参阅node_geo_mesh_to_curve_cc
命名空间。
这种特定于文件的命名空间方法对节点特别有效,因为通常只有单个函数被暴露(例如register_node_type_geo_mesh_to_curve
)。大多数其他源文件暴露了多个函数,通常暴露的函数与本地函数交错。在这种情况下,它不太方便,因为文件特定的命名空间必须多次打开和关闭,正如您在[这篇文章]中看到的[ 3]测试(https://projects.blender.org/blender/blender/pulls/110598)。
使这种方法更方便的潜在解决方案可能是自动插入命名空间范围。或者,可以组织源文件,以便将本地函数全部分组在一起,这样只需要一个命名空间范围。另一种方法是拆分源文件。例如,类似于我们每个文件有一个节点,我们也可以每个文件有一个运算符。如果没有统一构建或预编译的标头,拥有许多小源文件会导致大量的解析开销,但有了这些,问题就不那么大了。
使用统一构建时,必须决定每个单元应该串联多少个文件。这是一个权衡。小单元可以允许更多线程同时编译单元,代价是更多的解析和代码生成冗余。大单元减少了冗余工作,但总体上可以使用的线程更少。当系统只有几个内核时,或者当要编译的文件总数非常大,因此所有内核都会很忙时,大单位是有益的。实际上,创建单元可以使用cmake实现自动化。
通常,一个只在单个源文件中工作。当只更改一个文件时,使用统一构建会导致更长的重新编译时间,因为同一统一中的所有其他文件也会重新编译。这通常不是问题,但当其他文件之一碰巧需要很长时间时,例如,因为它使用openvdb
。这可以通过在cmake中使用SKIP_UNITY_BUILD_INCLUSION
来解决。它允许排除已知从单元编译缓慢的文件,也可以为当前正在编辑的文件临时添加。
使用SKIP_UNITY_BUILD_INCLUSION
,也应该可以开始在模块中逐步逐个文件引入统一构建。它还使在模块中受益于统一构建成为可能,这些模块有一些文件很难为统一构建做准备。
有时,当使用统一构建时,IDE更难提供良好的自动完成。对于使用cmake生成的编译命令(CMAKE_EXPORT_COMPILE_COMMANDS
)的IDE,有一个单独的构建文件夹来生成编译命令可能是有益的。该构建文件夹可以禁用统一构建和预编译标头。
使用统一构建也可以使链接器的工作更容易。这是因为链接器必须解析更少的符号表。在每个单元中,作为编译过程的一部分,符号已经变得唯一。这也减少了生成的机器代码的总大小,从而减少了构建文件夹的大小。
分布式构建
当工作PC之外有更多的计算资源可用时,可以考虑使用分布式构建。这些不会减少冗余(实际上可能会增加冗余),但仍然可以通过利用更多的并行性来改善编译时间。我自己对此没有太多经验,但像distcc这样的工具 8使用Blender应该不会太难。
分布式构建通常只在并行构建大量文件时提供帮助,因此在处理单个源文件时效果较差,每次更改后只需要重新编译少量文件。
C++20模块
从C++20模块中可以预期的编译速度可能类似于我们从预编译标头中获得的速度。因此,解析开销可以减少,但代码生成冗余可能仍然存在。
与预编译标头相比,模块的好处是源文件保持独立,并且并非所有模块都被迫包含相同的预编译标头。因此,最终无法自行编译的源文件的问题并不存在。
当然,缺点是我们还没有使用C++20。即使我们会使用它,我们的构建链也不太可能完全支持它们。即使他们这样做,我们可能仍然必须对我们的代码库进行相当重大的更改才能使用它们。
也就是说,切换到C++20也可以使[编译总体上变慢 74]。目前还不清楚使用模块是否可以抵消这种放缓,或者如果C++20实现变得更加成熟,这种放缓是否会消失。
工具
就像分析程序的运行时性能一样,也有测量构建时间的工具。这些可以用来进行有根据的猜测,哪些变化可能对构建时间的影响最大。这些工具最有助于识别应该避免包含的标头或代码生成占整体构建重要份额的函数。
- ClangBuildAnalyzer 59
- 在Visual Studio中构建洞察力 20,SDK 1,SDK示例 2
- 包括-你-使用什么 65
请随时建议我在这里应该添加的更多工具。
总结摘要
始终使用适合您的最快的链接器。这可能是你为改善Blender的日常工作所能做的最重要的改变。前瞻性声明有时可能有用,但需要严格的纪律才能获得有意义的编译时间改进。在C++中比在C中要难得多。拆分标头可能有利于提高代码质量,但可能没有足够的编译时间来证明在它们上花费太多时间是合理的。
预编译的标头在具有许多类似文件的模块中是有意义的。当使用统一建筑时,它们的有效性也会大大降低。总体而言,统一构建是减少冗余工作从而缩短编译时间的最有效工具。一些代码更改是必要的,但其有效性很容易衡量。
对于拥有资源的人来说,分布式构建是一件好事。C++20模块可以像预编译的标头一样提供帮助,但在可预见的未来是遥不可及的。
还有几点
- 有一件事没有提到,那就是ccache。我发现,在主分支和一些可能离它很远的随机分支或拉取请求之间切换时,它有很大帮助。回到主线要快得多。
- 此外,我还有多个git工作树和构建文件夹,这有助于减少重新编译量。一个跟踪主要,一个在bcon3期间跟踪即将发布的版本,另一个尝试随机拉取请求。不过,你很容易对自己正在编辑哪个分支或正在运行哪个构建感到困惑。
- 当我需要更新一个我目前没有的分支时,我会做的一个小技巧,例如
git fetch origin main:main
。这可以防止签出可能需要更多时间重新编译的分支的旧版本。 - 对于模具来说,各种Linux发行版中的软件包版本太旧了。新版本有重要的错误修复。构建自己并在CMake配置中设置路径相对简单。macOS上的模具现在也宣布免费,尽管Xcode 15也包含一个新的链接器,速度也相当快。
- 也许有点令人惊讶的是,只是[为新的C++版本启用构建标志 20]可以增加编译时间。因此,虽然C++20模块可能会有所帮助,但总体上并不明显更好。
其他工具
Include-What- You-Use
我使用了谷歌的Include-What-You-Use工具,这有很大的优势。它有助于正确使用标头,即哪些#includes进入标头,哪些在实现中,哪些应该是前向引用等。
cmake
也许这个提示太明显了,但对我来说,只通过停用尽可能多的cmake编译标志来构建我正在处理的内容是非常有帮助的。使用make lite的重建时间比使用make时快约3-4倍。
缺点是依赖项并不总是很清楚,即使代码编译并运行,您也可能会获得难以调试的意外结果。
Unity Build
在Unity Builds上,我个人的建议是,如果可能的话,就“说不”。面对许多缺点,您在为单个构建计时时所感知的收益将丢失:
- 时间追逐幻影编译错误出现在统一构建中,而不是典型的构建中,反之亦然。
- 由于增量编译/链接性能较差而损失的时间(可以通过自适应性转TU部分解决,但这会遇到下一个缺点)
- 时间损失,因为您现在已经失去了构建的确定性,并且无法再通过网络、本地或通过任何其他通用构建缓存机制轻松缓存中间对象文件
我曾在四个不同的AAA游戏引擎中工作过,所有这些引擎最终都采用了统一构建,以至少部分缓解构建时间的性能困境。虽然在短期内,团结建设是毋庸置疑的绷带,但它们会导致一长尾的问题,需要不断关注和维护。说我讨厌团结建设是温和的,但我不相信在许多每天与他们打交道的开发人员中,我是唯一一个有这种反应的人。
如果可能的话,我会鼓励适当的标题分离,避免巨大的模板函数,尽可能积极使用前向声明,以及已经提到的许多其他要点。一旦你团结起来,你就不会回来了,许多构建时间问题不太可能在以后真正得到解决。
关于C++20模块的主题,请注意,目前C++20模块和MSVC DLL之间的交互性很差。我在这个SO答案中写了这个,以供参考:C++模块和动态链接的预期关系是什么?-堆栈溢出 13。
统一构建
我知道一些开发人员不鼓励统一构建。我想我理解统一建设的缺点,但我认为这些可以用我在帖子中描述的方法来解决。有时更容易,有时更难。如果某些特定项目无法解决它们,我也不会使用统一构建。
时间追逐幻影编译错误出现在统一构建中,而不是典型的构建中,反之亦然。
我们使用统一构建已经快两年了,我认为它节省的时间远远超过修复编译错误的成本。我只是猜测,但会说这是按节省的几周和花费小时的顺序。除了偶尔缺少的包含外,我们真的不会遇到任何构建错误,这些错误通常很明显,易于修复。实际上,我刚刚数了一下,发现<15个提交自引入以来修复了统一构建的问题,所有这些都非常明显(例如 2)。
这之所以可能,只是因为我们通过为每个文件使用单独的命名空间来避免符号碰撞。如果出于某种原因这是不可能的,那么团结建设确实是一个糟糕的选择。
由于增量编译/链接性能较差而损失的时间(可以通过自适应性转TU部分解决,但这会遇到下一个缺点)
当然,这在很大程度上取决于您正在做什么样的工作,因为这会影响您在更改后通常必须重新编译多少文件。当更改影响许多翻译单元的标头时,统一构建的重建速度通常更快。当在单个源文件中长时间工作时,也必须一直编译其他文件可能会很烦人。就我个人而言,这从来都不是问题,但我知道其他人遇到了这个问题。我提到cmake的SKIP_UNITY_BUILD_INCLUSION
可以用来解决这个问题。
时间损失,因为您现在已经失去了构建的确定性,并且无法再通过网络、本地或通过任何其他通用构建缓存机制轻松缓存中间对象文件
我使用的唯一缓存机制是Ccache,它能够完美地缓存中间对象文件。如果您有使用分布式构建的奢侈,情况可能会更糟一点,但它也可能更好,这取决于你正在做什么,因为需要传输的数据更少。当然,Unity构建总是可选的。
它们导致一长长的问题,需要不断关注和维护
还不能谈论问题的长尾,也许当我变得更聪明时,问题就会到来。至少对于我们在Blender中的设置,我认为我们不必不断关注它,维护也很少。前向声明和标题分离通常不会有长的问题,但它们肯定需要持续的关注和维护。
- 再谈统一构建
也许我错过了一些东西,这确实有点悲伤,但afaik统一构建是真正消除解析和代码生成阶段冗余的唯一解决方案。当我们第一次引入统一构建时,我们测量了2-4倍的改进 1鉴于它给我们造成的问题很少,我不能理性地说不。
再次澄清一下:我认为按原样进行一个项目并实现统一构建是一个非常糟糕的主意。然而,通过一些前期时间投资,这可能是减少构建时间的非常有价值的方法。通过前期时间投资,我指的不仅仅是启用统一构建和解决出现的问题,而是实际上以与统一构建良好合作的方式构建代码。有人可能会说,仅仅为了统一构建,代码的结构不应该不同,但我认为这实际上与例如适当的标头分离非常相似,避免了巨大的模板和使用前向声明,当然,所有这些都可以而且通常应该完成。
我很好奇,你处理的游戏引擎在出现时是否只是启用了统一构建和固定名称冲突,还是它们实际上构建了代码以与统一构建很好地工作?
我使用的唯一缓存机制是Ccache,它能够完美地缓存中间对象文件。
我绝对指的是一个分布式缓存解决方案,该解决方案将在开发人员之间散列TU内容。Unity构建往往会中断,因为更改文件结构(添加、删除、移动文件)将导致不同的TU被破坏。当然,这有“解决方案”,这导致了Unity TU组装方式的额外复杂性。我同意,如果对分布式缓存机制没有兴趣,这种缺点就不那么明显了。
也许我错过了一些东西,这确实有点悲伤,但afaik统一构建是真正消除解析和代码生成阶段冗余的唯一解决方案。
可以说,我认为最好的办法是在标题中尽可能少地使用代码。你很早就提到了类型擦除,我认为这是最好的方法。可以说,如果像STL这样的库在模板中执行类型擦除,那么编译速度会快得多,然后下降到正向声明的类型擦除实现,正好位于一个翻译单元中。
我很好奇,你处理的游戏引擎在出现时是否只是启用了统一构建和固定名称冲突,还是它们实际上构建了代码以与统一构建很好地工作?
名称冲突在出现时确实被修复了,尽管有一些软惯例试图避免这种事情。不过,该解决方案在每个引擎中的味道都不同,尽管由于存在自定义预处理器,在某些引擎中特别棘手(值得注意的是,虚幻引擎的自定义预处理器不支持命名空间,它们是完全自适应统一构建)。
无论如何,如果它对你有用,它对你有用!由于Unity的建立,我个人遭受了多年的痛苦,但我完全理解,我的经验绝不是普遍的,这种经历的全部程度并不适用于所有情况。