单一来源项目结构的缺点是啥?

Posted

技术标签:

【中文标题】单一来源项目结构的缺点是啥?【英文标题】:What are the drawbacks of single source project structures?单一来源项目结构的缺点是什么? 【发布时间】:2017-09-20 10:25:57 【问题描述】:

我是当前公司的新人,正在从事一个由我的直接团队负责人编写的项目。该公司通常不使用 C++,但我的同事用 C/C++ 编写了富有成效的代码。只有我们知道如何用 C++ 编写代码(我和我的领导,所以没有第三意见可以参与)。

在我对项目有了足够的了解后,我意识到整个结构是……特别的

它实际上由一个编译单元组成,其中 makefile 将 main.hpp 列为唯一源。

然后这个头文件包含了项目所包含的所有源文件,所以它看起来像是一个非常大的列表:

#include "foo.cpp"
#include "bar.cpp"

在尝试理解其背后的逻辑时,我意识到这确实适用于这个项目,因为它只是一个界面,每个单元都可以在不访问任何其他单元的情况下进行操作,有时我问他为什么他是这样做的。

我对这个论点产生了防御性反应

嗯,它正在工作,不是吗?如果您认为这对您更好,您可以按照自己的方式进行操作。

这就是我现在正在做的事情,仅仅是因为我在思考这个结构时确实遇到了麻烦。所以现在我将“通常”的结构应用到我现在正在编写的实现中,同时只对整个项目进行强制性更改,以展示我将如何设计它。

我认为有很多缺点,从通过自己的项目结构混合链接器和编译器作业开始不能很好地服务,直到优化可能会导致冗余或模糊的结果,更不用说干净的构建该项目大约需要 30 分钟,我认为这也可能是由结构引起的。但我缺乏命名真实问题的知识,而不仅仅是假设的问题。

作为他的论点,“它按我的方式工作,不是吗?”确实如此,我希望能够向他解释为什么无论如何这是一个坏主意,而不是作为一个新的挑剔的家伙过来。

那么,这样的项目结构实际上会导致哪些问题呢? 还是我反应过度了,这样的结构完全没问题?

【问题讨论】:

恕我直言-保释。与这样的人一起工作将是一场噩梦。 只有一个编译单元意味着您必须在任何时候重新编译所有内容,即使是最轻微的更改。具有多个单元的良好分离结构有助于大大减少编译时间。 我知道有些项目将所有源代码放在同一个单元中,以实现额外的编译优化并挤出一些额外的性能。然而,这只是准备发布的一部分。开发仍以适当的多单元结构完成。 哦,对了,那只是一个大胖文件。文件有多大?现在我明白你的问题是为拥有多个编译单元找到一个很好的论据,只是试图了解这种情况。 @dhein 我想说最好的论点是编译时间(正如答案中提到的this),其次是更容易阅读,这在程序员中通常被低估了。 【参考方案1】:

更不用说项目的(干净的)构建需要大约 30 分钟

主要缺点是对代码任何部分的更改都需要从头开始重新编译整个程序。如果编译需要一分钟,这可能并不重要,但如果需要 30 分钟,那会很痛苦;它破坏了进行更改->编译->测试工作流。

更不用说项目的干净构建需要大约 30 分钟

拥有单独的翻译单元实际上从头开始编译通常会慢一些,但您只需要在每个单元发生更改时单独重新编译,这是主要优势。当然,通过在所有翻译单元中包含大量且经常变化的标题很容易错误地破坏这一优势。单独的翻译单元需要小心处理。

如今,对于多核 CPU,从头开始构建的速度会因多个翻译单元允许的并行性而得到缓解(如果单个翻译单元的大小恰好达到最佳点,甚至可以克服缺点,并且有足够多的核心;您需要进行彻底的研究才能找到答案)。

另一个潜在的缺点是整个编译过程必须适合内存。只有当内存超过您的开发人员工作站上的可用内存时,这才是一个问题。

总结:问题在于单一大量源文件方法不能很好地扩展到大型项目。


现在,为了公平起见,谈谈优势

优化可能会导致冗余或模糊的结果

实际上,单个翻译单元比单独的翻译单元更容易优化。这是因为某些优化(尤其是内联扩展)无法跨翻译单元进行,因为它们依赖于当前编译的翻译单元中不存在的定义。

由于流行编译器的稳定版本中提供了链接时间优化,因此这种优化优势已得到缓解。只要您能够并且愿意使用现代编译器,并启用链接时间优化(默认情况下可能不会启用)


PS。将单个 source 文件命名为扩展名为 .hpp 非常不合常规。

【讨论】:

关于内存的有趣点,还没有考虑过。关于唯一的源文件扩展名是.hpp,我发现这个 source 文件实际上甚至看起来像一个标题而不是一个源文件,这更加不合常规。我刚刚意识到,在对 makefile 进行更改时就是这种情况。 @dhein 好吧,这只是这种方法的结果。单一源文件只是编译过程的一个工具,而不是应该包含程序逻辑的东西。 其实如果你有一台多核机器你可以使用 make -jN 或者 ninja 来并行编译几个编译单元,Visual Studio 将至少并行化单独的项目,即使没有分布式构建,所以我不会限制分布式编译场的这一优势。 @PaulR 我想象编译器在编译单个编译单元时也可以跨多个内核共享其负载。也许我错了。 对于 Visual C++,我认为你可能是对的,如果我正确地记得编译期间的 CPU 负载(但这可能只在链接时间优化期间应用),但我不认为 gcc 和 clang 使用多个线程(同样可能除了某些形式的 LTO)。另见softwareengineering.stackexchange.com/questions/322494/…【参考方案2】:

首先我要提到的是具有单一编译单元的项目的优点

大幅缩短编译时间。这实际上是切换到 SCU 的主要原因之一。当您有一个带有n 翻译单元的常规项目时,编译时间将随着每个新翻译单元的添加而线性增长。虽然使用 SCU,它会以对数方式增长,并且向大型项目添加新单元几乎不会影响编译时间。 编译内存减少。磁盘和内存。 “大”翻译单元显然比每个单独的“小”翻译单元只包含项目的一部分占用更多的内存,但是它们的累积大小将大大超过“大”翻译单元的大小。 一些优化好处。显然,您会自动获得“所有内容都内联”。 不再担心“从头开始编译”。这非常重要,因为它是 CI 服务器执行的操作。

现在来说缺点:

开发人员必须保持严格的标头组织纪律。标头保护、#include 指令的一致顺序、强制包含当前文件直接需要的标头、正确的转发、一致的命名约定等。问题是没有工具可以帮助开发人员解决此问题,甚至标头组织中的轻微违规可能导致混乱的失败构建日志。 项目中文件总量的潜在增加。详情请见this answer。 没有更多"it's compiling" excuses for wooden sword fencing。

附言

在您的情况下,SCU 组织有点“软弱”。我的意思是该项目仍然有不正确的标题的翻译单元。这种情况通常发生在将旧项目转换为 SCU 时。 如果使用 SCU 构建项目需要大约 30 分钟,我觉得这不是项目组织的错误(可能是防病毒、没有 SSD、递归模板膨胀),或者如果没有 SCU 则需要几个小时。 一些数字:编译时间从约 14 分钟减少到约 20 秒,可执行文件大小减少 3 倍(根据我的经验将现有项目转换为 SCU) 实际用例:CppCon 2014: Nicolas Fleury "C++ in Huge AAA Games", Chromium Jumbo / Unity builds ("is can save hours for a full build") 我可能有点夸大其词,但在我看来,“多个翻译单元”(以及静态库)的整个概念应该被抛诸脑后。

【讨论】:

“这样的项目结构”你指的是哪一个?正如你所说的时间减少然后解释它的日志不是线性的,什么不是减少AFAIK。不错的答案,但如果你能更清楚地说明你指的是什么,我会很高兴:) 谢谢,很好的输入,非常有趣。但我不确定您是在总体上谈论 SCU 还是 SCU 还是针对我描述的具体案例?还是他所做的实际上是SCU的正确概念?如果不是,您能否详细介绍一下错误实施 SCU 的可能问题?虽然这目前很有趣,但它并不能帮助我找到反对现有结构的原因(一般不是 SCU) @dhein 我说的是 SCU。但是我将您拥有的那种项目(即包含大量.cpp 文件的单个文件的编译)称为“软煮”,因为它有点“偷工减料”的方法。维护庞大的列表似乎是一个相当大的负担。正确的 SCU(如我所见)意味着绝对将所有内容编写为 header-only library (即编译仅包含带有入口点的“application header-only library”标头的单个文件,没有任何类型的“大名单”)。 ““多个翻译单元”(以及静态库)的整个概念应该留在过去” - 这是一个非常有力的声明。它不应该从提供的论据/案例中得出。是的,单体构建具有某些优势。我发现这种概括是有害的。 @dhein 关于这个主题的一个很好的例子是sqlite 项目。他们将代码发布为一个大的.c 文件 - 但是如果您浏览代码,因为它是开发人员和版本控制的,它会被正确地组织到一个单独的.c 文件中。

以上是关于单一来源项目结构的缺点是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 angular-cli (6.x) 创建单一仓库项目结构

Python 开源项目的正常结构是啥?运行测试的首选方式是啥? [关闭]

Grunt 项目推荐的目录结构是啥

MVC与BS的区别,他们的优点及劣势是啥?

React+Redux 项目的标准文件夹结构是啥?

打包结构化 Spring 项目的最佳实践是啥?