【问题标题】:Improving compilation times of a monolithic .NET assembly - incremental compilation?改进单片 .NET 程序集的编译时间 - 增量编译?
【发布时间】:2018-10-02 13:13:07
【问题描述】:

我们正在使用 .NET Framework 4.6,目前有一个包含我们大部分应用程序的单体 DLL。它有大约 700k 行代码。每当我们进行更改时,都需要一分钟多的时间来重新编译。如果可能,我们希望加快速度。

一种选择是将单体应用程序分解为多个对等程序集,其中任何一个都不会相互依赖(项目之间共享代码的单个通用程序集除外)。因此,如果更改仅限于单个程序集,编译器只需重新​​编译这个较小的 DLL。此外,这些对等程序集可以并行编译,同样有可能减少编译时间。

其他人认为 .NET Core 2.x 的新“增量构建”功能可以从根本上增加单体架构的构建时间,这意味着编译器只能重建单体架构中发生变化的部分,而忽略其余的。

真的是这样吗,或者这不是新的增量构建功能的工作原理?

或者有人对缩短编译时间的最佳方法有任何其他建议吗?

【问题讨论】:

  • “一种选择是将整体分解为多个对等程序集,因此如果更改仅限于单个程序集,编译器只需重新​​编译这个较小的 DLL。”不要那么肯定。我在一个项目中工作时这样做非常糟糕,以至于由于依赖关系而导致单个更改意味着重新编译 5 个项目并且花费了 3 多分钟。它甚至没有 50k LOC
  • 我想这里的关键词是“peer”——我们提议将单体分解成的所有程序集都不会相互依赖。它们将是编译树中的叶子。所以我的假设是对对等点的更改应该与其他对等点隔离,因此只需要重新编译对等点本身。这不正确吗?你能分享更多关于你的经历吗?
  • 这就是为什么我说它出错了:) 只要他们没有依赖关系,你应该没问题
  • @MikeChamberlain:就其价值而言,您在这里提出的建议对我来说听起来完全标准。我们有几个巨大的应用程序,由(有时)数百个单独的程序集组成,没有 Camilo 暗示的构建问题。
  • 增量构建功能与否,如果单体应用有不相关的“组件”,我觉得它们应该被移动到单独的组件中,只需通过SOLID's S。但是,据我了解,“无依赖场景”似乎不太可能,即使在这种情况下,我仍然认为可以应用 SOLID 原则来提高应用程序的性能、结构和维护。

标签: c# .net compilation


【解决方案1】:

Incremental builds 可以帮助您处理单个单体 DLL。在 2.0 中引入了它们,但主要侧重于检测依赖项更改 - Visual Studio 已经在您的案例中为您做这件事。

然而,来自What's New in .NET Core 2.1

使用长时间运行的 SDK 构建服务器,这些服务器是跨越单个 dotnet 构建调用的进程。它们消除了每次运行 dotnet build 时 JIT 编译大块代码的需要。

我目前无法准确测试这对于大型项目的构建和小范围的孤立代码更改可能意味着什么,但我建议尝试一下。

更新,我试图准确地测试这可能意味着什么,但不幸的是没有一个 700k 行的项目可以测试。我使用了this MS example project 并运行了 2 个副本 - 一个组合成一个 dll,另一个分开。我做的每一个测试,样本“整体”组合项目运行得更快,但我假设这些是无效的结果,因为组合项目几乎不被认为是“整体”。

我在这个示例“单体”中发现了一件有趣的事情:无论是对视图进行微小更改,还是对项目进行彻底更改,我都没有看到新编译时间之间的差异。这让我质疑这个新的构建服务器到底是用 JIT 的东西缓存了什么。也许再一次,这只是因为我的“单体”并不大。

最后,我找不到任何证据证明您真正的单体项目会在 .NET Core 2.1 更改期间提供更快的构建。我可以看到将其拆分为几个功能性项目带来好处的证据。底线是,没有改变的项目不会被重新编译。

最后,在您的情况下,您有一个单一的 DLL,这是问题的主要部分。您和其他人在 cmets 中建议的所有其他内容将大大增加您的构建时间,因为如果某些不经常更改的关键代码区域可以分解到其他项目中,则它们不会在每次编译时重新构建。如果它们不经常更改非常,它们甚至可能被拉入 nuget 包并托管在私有 nuget 服务器上。然后它们就不会被编译——即使是在全新的开发环境中。

希望这会有所帮助!

【讨论】:

    【解决方案2】:

    摆脱整体设计并考虑将属于一起的部件放入单独的组件中是个好主意 - 所以这是我建议您采用的方式。

    实现这一点的方法是(在 Visual Studio 2015 - 2017 中):

    1. 打开您的解决方案
    2. 对于您要添加的每个程序集,在您的解决方案中创建一个单独的项目(右键单击解决方案以打开上下文菜单,然后选择 add --> new project ... 在上下文菜单中,然后选择 Visual C# -> 类库)
    3. 为您创建的每个程序集添加对主项目(这是您的单体应用程序)的引用(右键单击引用,然后选择添加引用... 并浏览 Projects --> solution 导航器以选择它)
    4. 右键单击主项目,然后选择 Build Dependencies -> Project Dependencies... 在对话框中,确保在下拉列表中选择了您的主项目。勾选您添加的每个程序集。现在您的主项目依赖于程序集,并将按正确的顺序编译。
    5. 将主项目中的类移动到程序集项目中(剪切和粘贴它们)。如果这会创建任何其他依赖项,请打开 Build Dependencies -> Project Dependencies...,这次是依赖于不同程序集的项目,并勾选它所依赖的程序集。
    6. 右键单击解决方案并选择重新构建解决方案。如果您做的一切都正确,那么编译应该会成功。否则,请修复出现的任何错误(例如,由于缺少参考资料)。检查您是否遗漏了任何用于导入命名空间的 using 语句。

    从现在开始,您只需构建一个已更改的程序集(右键单击项目,然后选择“构建”)。如果有任何依赖,编译器会自动按照正确的构建顺序进行编译。

    由于您已将整体设计分解为更小的程序集,因此每个程序集的编译速度都非常快,您只需要重新编译已更改的程序集。

    那些不依赖于其他程序集的程序可以并行编译 - 因此,如果您想运行不同的编译器进程,您可以考虑创建一个执行此操作的批处理。项目依赖项对话框可帮助您识别可以并行编译的程序集。

    “构建顺序”选项卡向您显示它们应该运行的顺序,“依赖项”选项卡显示每个程序集的哪些其他程序集依赖。通过这种方式,您可以找出是否有任何“分支”可以与其他“分支”并行编译(“分支”不是指源代码控制的分支,而是您的解决方案的分支:想想一棵树,其中主项目是根 - 每个分支都由您的解决方案的一系列程序集组成,可以并行编译)。

    【讨论】:

      【解决方案3】:

      我同意分手。但是有很多方法可以分解解决方案。我认为你应该分阶段进行。

      第 1 阶段 - 现在打破它的最快方法。

      获得最快结果的最常用方法:

      1. 所有模型 - 创建模型库项目并将所有模型文件移至其中。让您的单体应用参考模型项目。根本不要更改文件或名称空间。只需从一个项目剪切并粘贴到另一个项目。如果任何模型类是私有的,请将它们设为内部(在项目中查找和替换)并将 InternalsVisibleTo 属性添加到您的整体程序集。移动所有模型类的预计时间:20 分钟。

      2. 所有接口 - 创建一个接口库项目并将所有接口类文件移至其中。让您的单体应用参考模型项目。根本不要更改文件或名称空间。只需从一个项目剪切并粘贴到另一个项目。如果任何接口类是私有的,请将它们设置为内部的(在项目中查找和替换)并将 InternalsVisibleTo 属性添加到您的整体程序集。移动所有接口类的预计时间:20 分钟。

      3. Generated Code - 创建 GeneratedCode 项目。将所有 100% 生成代码的类移至该项目。当然,这是一个假设,因为你有 700k 行代码,所以很多类都是完全生成的。如果您没有生成代码,请跳过此代码。移动所有生成代码的预计时间:1 小时。

      4. 扩展方法 - 创建一个扩展项目,并将所有扩展方法类移至该项目。移动所有模型类的预计时间:30 分钟。

      5. FixedLogic - 创建一个固定逻辑项目。找到所有你很少接触的类——在一个他这么大的项目中,可能有数百个多年来没有接触过的类文件。您无缘无故地一遍又一遍地重新编译这些类。你必须弄清楚那些是什么。希望您可以在源代码管理中轻松看到这一点。预计时间:2 小时找出源代码管理中的查询。 10 分钟将这些文件移动到新项目。

      6. 为您的版本保留 1 个 dll。现在,我假设您有一个构建、部署、安装过程。如果没有,请忽略此步骤。如果你这样做了,而且它很复杂,你可能想要将 dll 添加到现有进程中,因为这会增加这些任务的额外时间。因此,请确保所有文件都以仍然产生 1 个 dll 的方式编译。见这篇文章:https://docs.microsoft.com/en-us/dotnet/framework/app-domains/how-to-build-a-multifile-assembly。实施本文的预计时间:4 小时。

      主要目标:首次构建后,后续构建仅构建有更改的项目,从而显着减少构建时间。

      第 2 阶段 - 一种更好但长期的方法。

      花点时间以有意义的方式分解代码。仅当您触摸代码时才执行此操作。

      1. 分析您的项目并列出您的代码所涵盖的领域或领域。这只是文本文件中的一个简单列表。你可能有很多域。

      2. 将类文件分配给这些域,但也将类文件标记为模型、接口、逻辑。

      3. 下次您在域中接触文件时,创建三个项目:Domain.Models、Domain.Interfaces 和 Domain.Logic。将代码移动到正确的项目中。

      主要目标 - 关注点分离

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-13
        • 1970-01-01
        • 2017-05-16
        • 1970-01-01
        相关资源
        最近更新 更多