和很多人一样,我们的C++项目越来越大,它最终达到了可维护性开始受到关注的地步,这主要是由于构建时间:即使使用ccache,任何更改都需要30秒到5分钟的时间进行重建和测试(实际上,“链接”似乎是一个耗时的阶段,但我们仍在进行研究)。
该项目主要由一个大型的科学图书馆和一些利用它的应用程序组成。例如,有一些应用程序可以增强仿真模型或语言绑定(python),以便集成到其他软件包中。
现在,将库分成更小的库似乎很自然:代码已经组织在“模块”中,所以这不会是一个问题。问题在于依赖关系管理:我们可以将整个项目看作是一个大型达格,其中每个节点都是一个库模块,或者是外部库(由于项目性质,其他科学图书馆存在许多外部依赖)。
因此,理想情况下,在处理模块时,我们只希望重建该模块,再加上相关的测试应用程序,并将其与其依赖项链接起来。这将大大加快开发周期。
因此,我们的想法是将每个“模块”作为一个独立的项目来处理,该项目将单独构建到一个(静态)库中。我对这种方法有两个关切:
我一直在研究几种可能有助于我的方案的工具。Git子模是一种选择,但它并没有解决我上面的问题。阿帕奇常春藤似乎不适合我正在努力实现的目标。我怀疑答案涉及到一些强大的构建系统(我们正在使用qmake),因此我研究了cmake (但我发现它的脚本语言很有限,很难学习),然后是巴克和短裤。
我仍然不确定这是否是前进的道路,所以如果有人能分享建议和/或经验,我将不胜感激。
发布于 2017-11-09 05:45:10
你在问题中提到了静态库。我猜测(可能是错误的)您为Linux编写了一个程序。
如果可能的话,您应该使用共享库。然后,您需要更少的链接时间(以牺牲运行时链接(通常在启动时足够快)为代价)。
在开发过程中,我发现我需要库C中的一个新特性,所以我继续编写代码。在继续开发之前,我现在必须手动重新构建库C,然后是库B,最后是库A。
共享库不应该是这种情况(只要它们的API没有改变)。
考虑使用插件插件的一些体系结构,以及它们的动态加载 (例如,在POSIX上使用dlopen )。
似乎“链接”是一个耗时的阶段。
您需要确保情况确实如此(例如,通过传球 -time,甚至-ftime-report to g++)。然后,您可以将金链接器和/或visibility函数属性与g++一起使用。
确保您的子模块或库的API不会经常更改。
避免C++源文件太小,例如,宁愿有10个C++源文件,每个源文件有2000行,而不是每条只有200行的100个C++源文件。C++源文件可以定义和实现几个(相关的)函数或类。请记住,C++没有模块,而且大多数标准标头(以及您自己的)都是相当大的文件(例如,在我的Linux桌面上,#include <vector>扩展到大约10 10KLOC );编译时间重要的是预处理表单的大小,模板扩展也需要大量时间。另见这。
顺便说一句,您还需要使用一个好的建筑自动化工具(例如忍者、GNU制造 -通过make -j 8启用并行化等等)。您还可以使用分布式构建工具(远距离、冰淇淋、.)
您还可以(使用GCC)考虑使用预编译标头。然后,您可能希望有一个包含所有其他头文件的头文件。您可能需要使用PIMPL成语。
另见这个答案中的相关问题。
每次更改都需要30秒到5分钟的时间来进行重建和测试。
顺便说一句,5分钟的增量构建时间对我来说是相当小的。你为什么要抱怨?我已经到了90年代,在那个时候强大的Sun工作站上,构建了几乎一个小时的时间(几年来我独自写了几十万行的软件)。我有时也会为GCC自己做出贡献,即使是这些年,他们的重建时间也要花上几个小时。
发布于 2017-11-29 08:27:32
关于构建系统和共享库,已经有了很好的答案,但是我将从另一个角度来解决这个问题,我不认为人们会经常申请。
对我来说,将稳定的代码(不太可能需要进一步的修改)和不稳定的代码(当软件扩展时自然需要进一步修改的代码)分开是很有用的。如果您不能这样做,并将整个代码基看作是潜在的移动部分,即使接口完全稳定,我认为您正在做一些错误的事情。
我们已经倾向于在开源第三方库中自然地做到这一点。我们通常只构建它们一次,最好是创建一个dylib,以避免静态链接时间,只需在运行时查找符号并调用它们中的函数。我们不需要一遍又一遍地重新构建它们,因为我们没有兴趣接触它们的源代码,只使用可用的功能--基本上只构建一次并永远使用。在这方面,代码是100%稳定的(它没有任何改变的理由,至少在我们手中是没有的)。
因此,对于第三方,我亲自将它们放在"third_parties“子目录中,只有当我添加一个新的第三方库或用一个非常罕见的新版本替换旧版本时,我才会构建这个子目录,比如每隔几个月至少一次。我不会每天都在重建这些东西。
同样的事情应该适用于你自己的代码库。我有一件类似的事情,我有一个带有自己代码的"libs“子目录,并且很少构建结果库,因为代码非常稳定、可靠、高效、经过单元测试彻底测试、经过静态分析,而不是我将来需要改变的东西。它远离"libs“子目录之外的不稳定代码,该子目录确实需要每天进行更改,而这些代码确实会被反复重建。
因此,以这种方式分离代码库--如果稳定的部分很少(如果有的话)需要从经常需要更改的不稳定部分中进行更改--可能是组织代码库的有用方法--不仅在优化构建时间方面,而且为了更好地将稳定的包与不稳定的包分离开来,目标是尽可能多地获得稳定的代码,而您可以自信地说,这不会保证在不久的将来进行任何更改。
当您这样做,这些稳定的部分,您的代码库可以单独构建,远离不稳定的部分使用它们。不稳定的部分不应该花那么长的时间来构建,因为您的稳定库集会扩展和扩展,而不稳定的部分则会收缩。
然而,这往往意味着一些代码重复。为了能够创建一个稳定的映像库,它不能依赖,比方说,一些不稳定的数学库,或者对数学库的更改,如果不是进一步对映像库的源更改,则需要重新构建。所以我发现,有时候,图像库可以复制一些数学例程,以便独立于任何辅助数学库。在这种情况下,通过一定程度的逻辑复制将代码解耦,实际上可以帮助使这些包更加稳定,从而消除了它们不得不一次又一次地更改和/或重建的原因。如果代码经过了良好的测试,并且在未来的几年中运行良好,并且可能几乎不需要重建,那么库实现其独立性和稳定性所需的适度复制也不是什么问题。
此外,它有助于在您的接口中实现极简主义。如果你的形象库的目标是实现人类所能想象到的每一个图像操作,那么它的野心和独断专行的自然将保证永远不变。它永远不可能成为一个有这样的目标的稳定的图书馆。如果它的目标仅仅是提供基本的映像操作,甚至根本不提供使用库所提供的在库之外创建映像操作的能力,那么它就有可能实现一种完全稳定的状态(在不久的将来找不到任何改变的理由)。
所以不管怎么说,如果你要开始拆分一个代码库,我建议你开始分割那些稳定的,经过良好测试的部分,这样你很确定你不需要把它们从不稳定的部分中进一步修改,在那里你至少可以预见到一些将来需要改变的可能性。构建稳定部分应该从不稳定部分(经常重建)调用单独的构建过程(很少应用)。
https://softwareengineering.stackexchange.com/questions/360452
复制相似问题