湖面像什么| 荷花是什么时候开的| 拉黑粑粑是什么原因啊| 思想包袱是什么意思| 满城尽带黄金甲是什么意思| 湿疹是什么样子| nb什么意思| 野蛮生长是什么意思| 性冷淡吃什么药| 他们吃什么| 鳞状上皮增生什么意思| 脂蛋白高吃什么药能降下来| 复健是什么意思| 基数是什么| 质数是什么| 佝偻病是什么样子图片| 那敢情好是什么意思| 胸闷气短吃什么特效药| 游击战是什么意思| bally什么档次| 梦见和婆婆吵架是什么意思| 突兀什么| 安全监察是一种带有什么的监督| 股市xd是什么意思| 孕妇适合吃什么食物| 小暑是什么时候| 发烧了吃什么食物好| 鹅吃什么草| 一天中什么时候最热| 脸发麻是什么病的前兆| 艾滋病人有什么特征| 金国是什么民族| 早上起床口臭是什么原因| 做腹腔镜手术后需要注意什么| 红顶商人是什么意思| 送奶奶什么礼物好| 吃紧急避孕药有什么副作用| 婴儿蚊虫叮咬红肿用什么药| 喘不上气吃什么药见效| 水肿吃什么药| 侄女叫我什么| 脾胃虚是什么症状| 泌尿科主要看什么病| 胃属于什么科室| 子宫前置是什么意思| 骨髓炎是什么症状| 班长是什么军衔| 基诺浦鞋属于什么档次| hpv弱阳性是什么意思| 这是什么动物| hr是什么意思| 摔伤挂什么科| 耐信是什么药| 阿弥陀佛是什么意思| 紫玉是什么玉| 为什么海螺里有大海的声音| 宫颈炎有什么症状表现| 肩膀疼挂什么科室最好| 茯砖茶是什么茶| 做小月子要注意什么| 肾结石是什么| 改良碱性土壤用什么| 肝肿瘤不能吃什么| 吃海鲜不能吃什么水果| 吃什么都咸是什么原因| 牙套脸是什么样| 眼镜轴位是什么| 月亮杯是什么东西| 不可小觑什么意思| 供奉财神爷有什么讲究| 都有什么大学| 云丝是什么| 马蹄铁什么时候发明的| 探店是什么意思| 梦见别人拉屎是什么意思| 画蛇添足什么意思| mk是什么牌子| 梦见请别人吃饭是什么意思| 什么是菜花病| 草酸是什么| 肠炎挂什么科| 去医院验血挂什么科| 避孕药有什么副作用| 皮下囊肿是什么原因引起的| 舌系带长有什么影响吗| 什么是飞蚊症| 耳垂长痘痘是什么原因| dmp是什么意思| 2020是什么生肖| 哈伦裤配什么上衣好看| 肩胛骨缝疼吃什么药| 联袂是什么意思| 胆木是什么| 狗为什么不死在家里| 古人的婚礼在什么时候举行| 白居易号什么居士| 孕中期头疼是什么原因| penguin是什么意思| 怀孕孕酮低吃什么补得快| 早上起来手麻是什么原因| 煊是什么意思| 子息克乏是什么意思| 小囊肿是什么病严重吗| 318什么意思| 胰腺炎适合吃什么食物| 龙眼树上的臭虫叫什么| 频繁打哈欠是什么原因| 为什么不建议小孩吃罗红霉素| 一直不射精是什么原因| 用盐水洗脸有什么效果| 做头发是什么意思| 台湾有什么特产最有名| 什么什么的天空| 跳蛋什么感觉| 身心合一是什么意思| pop店铺是什么意思| 小孩黄疸是什么原因引起的| 伽蓝菩萨保佑什么| gcp是什么意思| 撬墙角是什么意思| msgm是什么品牌| 化验血能查出什么项目| 娘酒是什么酒| 2015年五行属什么| 眼睛有眼屎是什么原因引起的| 气性大是什么意思| 经常过敏是什么原因| 孕妇血糖高吃什么| 心动过缓吃什么药最好| 猫呕吐是什么原因| 什么是脱敏治疗| 内痔是什么| 晚上十一点半是什么时辰| 原研药是什么意思| 井代表什么生肖| 血液透析是什么意思| 牛肉和什么炒好吃| 花甲不能和什么一起吃| 上面白下面本念什么| 看抑郁症挂什么科| 急性咽喉炎吃什么药好得快| 刘邦字什么| 汗蒸有什么好处和功效| 意什么深什么| 57是什么意思| 皇帝菜是什么菜| 脚踝疼痛是什么原因| 胖大海配什么喝治咽炎| 兔头是什么意思| 什么是慢性病| 打喷嚏流清鼻涕吃什么药| 三叉神经痛用什么药| oz是什么单位| 门牙旁边的牙齿叫什么| 属马与什么属相最配| 腰间盘突出有什么好的治疗方法| 增生是什么意思| 筋膜炎吃什么药最有效| 花甲和什么不能一起吃| 肺阴虚吃什么中成药| 补充电解质是什么意思| 磨豆浆是什么意思| 什么洗发水最好| 绿豆可以和什么一起煮| 生物学父亲是什么意思| 又什么又什么| bees是什么意思| btc是什么货币| 俄罗斯特工组织叫什么| 骨密度是什么意思| 肝火旺是什么意思| 孔雀喜欢吃什么食物| 陈字五行属什么| 腹胀屁多是什么原因| 为什么不建议吃三代头孢| 焦虑症吃什么药好| 殿后和垫后有什么区别| 舌头鱼又叫什么鱼| 得偿所愿是什么意思| 激光脱毛对人体有没有什么危害| 猪心炖什么适合孩子| 天秤座女生什么性格| c罗全名叫什么| 咳嗽有白痰吃什么药好| 耳鸣用什么滴耳液| 阴囊湿疹吃什么药| 乌龟爬进家暗示什么| 含羞草能治什么病| 公务员干什么工作| 消化酶缺乏是什么症状| 滴度是什么意思| 西柚不能和什么一起吃| 杜比全景声是什么意思| 缺维生素d吃什么| 梦到捡菌子是什么意思| 三个牛读什么字| 小腿抽筋吃什么药| 三奇贵人是什么意思| 什么人容易得骨肿瘤| 明天叫什么日| 偏光是什么意思| 恶心头晕是什么症状| 月经提前是什么原因引起的| 追求是什么意思| 鸭肚是鸭的什么部位| 肉毒为什么怕热敷| 吃什么药可以推迟月经| 秋后问斩是什么意思| 夏天吹什么风| 龙虎山是什么地貌| 一行是什么意思| 过敏性鼻炎喷什么药| 下午18点是什么时辰| 暮春是什么时候| 升结肠管状腺瘤是什么意思| 1996属鼠的是什么命| 铁观音是什么茶| 包干价是什么意思| 姑姑的弟弟叫什么| 酱油什么时候发明的| 九死一生什么意思| 乙肝两对半145阳性是什么意思| 胃食管反流病吃什么药| 牙齿为什么发黄| cd3cd4cd8都代表什么| 10月18日什么星座| 大便失禁是什么原因| 上海特产是什么| 卵泡不破是什么原因| s是什么车| 宫腔内偏强回声是什么意思| 骨肉瘤是什么病| 朝霞什么晚霞什么| 腹主动脉钙化是什么意思| 尿路感染是什么原因造成的| 十一月份属于什么星座| 女性尿道出血是什么原因引起的| 淞字五行属什么| 温碧泉适合什么年龄| 一马平川是什么意思| 回南天是什么时候| 京酱肉丝是什么菜系| 含锶矿泉水有什么好处| 用牙膏洗脸有什么好处和坏处| 航五行属什么| 汗疱疹用什么药膏最好| 低血压吃什么水果| 什么是焦距| 腰两侧疼痛是什么原因| 面首是什么| 机能是什么意思| 大拇指发麻是什么原因| 9月26号是什么星座| 脚臭用什么洗效果最好| 孙策是孙权的什么人| 河里的贝壳叫什么| 食道炎用什么药最好| 长期大便不成形是什么原因造成的| 中国劲酒有什么功效| 颈椎病有些什么症状| 心率低于60说明什么| 七十岁是什么之年| 冬瓜什么时候成熟| 最贵的玉是什么玉| 百度
打印
[其它应用]

《剑与魔法》绿色度测评报告

[复制链接]
8390|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
laocuo1142|  楼主 | 2024-3-6 09:57 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 laocuo1142 于 2024-3-6 09:58 编辑

有时候,也许你刚换了一份新工作,也许刚换了一个团队,也许团队中某个有经验的人刚离开,这时需要你来继承一个旧的 C++ 代码库:这个代码库既庞大又复杂,还很特别,经常以各种有趣的方式崩溃。一句话概括来说:它充满了各种遗留问题。

尽管如此,Bug 仍需要修复,奇怪的功能也还需要添加,你无法完全忽视或直接干脆让它消失——这个代码库很重要,至少对给你发工资的人来说是这样,所以它对你也很重要。

不过你不用担心,因为我在很多地方都有过同样的经历,有一种方法,它不会让你太痛苦,能让你真正修复 Bug、添加功能,甚至有朝一**还有望能重写它。

因此,接下来请和我一起回忆一下哪些方法对我有用,哪些方法绝对要避免。

我先声明一下,我并不讨厌 C++,它只是碰巧成为了人们滥用的语言之一,并导致了很多可怕的混乱。可怜的 C++ 只是受害者,C++ 委员会将在 C++45 中修复它,不用担心……跑题了,让我们回到眼前的话题。

下面是需要采取的步骤概述:

只对代码和构建系统做最小的改动,最好是不做改动,让它在本地运行。即使你十分手痒,也不要进行大的重构!

拿出“电锯”,“锯掉”一切和公司/开源项目宣传和销售特性无关的一切

通过添加 CI、linters、fuzzing、自动格式化等功能,让项目步入 21 世纪

最后,可以对代码进行小规模的增量修改,不断重复,直到你不再每天晚上都被应用被黑客入侵的噩梦惊醒。

如果可以的话,考虑用一种内存安全语言重写部分代码。

总体目标就是:花费最少的精力,使项目在安全性、开发人员体验、正确性和性能方面达到可接受的状态——记住这一点很重要,整个过程与“干净的代码”、使用新的热门语言特性等都无关。

好了,让我们开始吧!顺便说一下,本文所有内容都适用于纯 C 代码库或 C 和 C++ 混合代码库,如果你是这样的人,请继续阅读!

1



获得支持

你以为我会上来就比较不同的杀毒程序、编译标志或构建系统吗?不,在我们做任何工作之前,都要与人交谈,对吗?

软件工程必须是一种可持续的实践,而不是几个月或几年后就会倦怠。我们不可能在下班后或各种死亡行程中,独自一人完成这项工作!我们需要说服人们支持这项工作,让他们理解我们在做什么,以及为什么。这包括每个人:你的老板、同事,甚至是非技术人员。这样就算你去度假,回来后就会发现,当你不在办公室时,人们还在继续这项工作。

所有这一切只意味着:用一些简单的事实、合适的解决方案和一个时间表,以外行也能够听懂的方式解释问题。下面我给你简单举几个例子:

嘿,老板,上一个员工花了 3 周时间才编写好代码并做出了第一个贡献。如果我们能花最少的精力,在几分钟内完成,那不是很好吗?

嘿,老板,我快速组装了一个简单的 Fuzzing 设置,结果几秒内就让应用崩溃了 253 次。我想知道,如果有人在生产过程中对我们的应用进行这样的测试,会发生什么情况?

嘿,老板,最近修复几个紧急 Bug 花了几个人和两个星期的时间才部署到生产环境中,因为这个应用只能在一台服务器上构建,而这台服务器使用的是早在 8 年前就停止支持的旧操作系统了。哦,顺便说一下,一旦这台服务器死机,我们就再也无法进行部署了。如果能在便宜的云实例上构建我们的应用,那该多好啊?

嘿,老板,我们在生产中遇到了一个影响用户的隐秘 Bug,花了几周时间才发现并修复,原来是由于未定义的行为(“代码中很难察觉的问题”)破坏了数据。而当我在代码上运行这个行业标准的 linter(”发现代码中问题的程序”)时,它能立即发现这个问题。我建议,我们应该在每次修改代码时都运行这个工具!

嘿,老板,一年一度的审计就要到了,上次审计花了 7 个月才通过,因为审计员对他们所看到的不满意,这次我有办法让审计更顺利。

嘿,老板,刚刚新闻上说有一个安全 Bug,它可以解密加密数据并窃取机密,我认为我们可能会受到影响,但我不确定,因为我们用的加密库是手工制作的(”复制粘贴”),上面有一些未经任何人审查的改动。我们应该清理并设置一些功能,以便在出现影响我们的漏洞时自动发出警报。

相反,以下是你记得一定要避免的说法:

我们没有用最新的 C++ 标准,我认为应该停止所有工作,留两周时间来升级。不过我也不知道会有什么东西被破坏,因为我们没有测试。

我打算在一个单独的分支上修改项目中的很多东西,并为此工作数月。我相信它肯定会在某个时候被合并的!

我们打算从头开始重写这个项目,这需要几周的时间。

我们将改进代码库,但不知道何时能完成,也不知道具体要做什么。

好了,假设现在你已经得到了所有重要人物的支持,我们来回顾一下这个过程:

每一次改变都是小规模、渐进式的。应用程序之前能运行,之后也能运行。测试通过后,测试人员很满意,也没有任何更改被绕过。

如果需要紧急修复 Bug,可以照常进行,没有任何阻碍。

每项变更都是可衡量的改进,可以向非专家解释和演示。

如果整个工作不得不暂停或完全停止(因为优先级转移、预算原因等),与开始工作前相比,总体上仍有净收益(而且这种收益在某种程度上是可衡量的)。

根据我的经验,采用这种方法,你可以让每个人都满意,并能真正完成需要做的改进工作。

好了,让我们言归正传吧!

沙发
laocuo1142|  楼主 | 2024-3-6 09:57 | 只看该作者
2
写下你支持的平台

这一点非常重要,但很少有项目做到这一点。写在 README 中,这只是一个列出你的代码库正式支持的<架构>-<操作系统>对的列表,例如 x86_64-linux 或 aarch64-darwin。这对于在每一个平台上的构建都能正常工作至关重要,而且我们稍后还会看到,它还可以清理掉不支持平台上的不必要文件。

如果你想要更加高级一些,甚至可以写下架构版本,比如 ARMV6 vs ARMv7 等等。

这有助于回答一些重要问题,比如:

我们能否依靠硬件支持浮点数、SIMD 或 SHA256?

我们是否需要支持 32 位?

我们是否会在大二进制平台上运行?(答案很可能是:不会,过去不会,将来也不会)。

一个 char 能否是 7 位?

还有很重要的一点: 这份列表一定要包括开发者的工作站,这也就引出了我下面要说的内容。

3

在你的机器上构建

你一定会惊讶于有这么多 C++ 代码库,它们是成功产品的核心部分,能赚取数百万美元,却基本上无法编译。当然如果一切顺利,它们是可以编译的。但我说的不是这个,我说的是在你支持的所有平台上可靠、稳定地构建:没有什么“我花了三周时间终于编译成功了”这样的过程,它本身就是能运行。

讲一个小插曲,我以前非常喜欢空手道,每周要进行 3、4 次训练。我清楚地记得我的一位老师曾经对我说:“你还没有掌握这一招。有时你会,有时你不会,所以你就是不会。当你用勺子吃饭时,你会不会五次中有一次没吃到嘴里?”

作为一名软件工程师,我一直带着这个问题。“新功能有效”意味着每次都有效,而不是有 80% 的可能性有效,因此构建工作是一样的。

经验告诉我,快速高效地开发软件的最佳方式是在自己的机器上构建,最好还能在自己的机器上运行。如果你的项目非常庞大,这可能是个问题,因为你的系统可能没有足够的内存来完成构建。备选方案是:在某处租用一台大型服务器,然后再运行构建。虽然这并不理想,但总比没有好。

另一个障碍是需要一些特定平台的 API,例如 Linux 上的 io_uring。在这种情况下,可以在工作站上的虚拟机内实现一个临时接口进行构建——同样,这也并不理想,但有总比没有好。

我过去曾使用过上述所有方法,最终发现:直接在自己的机器上构建仍是最佳选择。

4

在你的机器上通过测试

首先,如果没有测试的话,要对代码进行任何修改都会非常困难。因此,在对代码进行任何修改之前,先编写一些测试,确保它们通过了再开始修改。最简单的方法是,捕捉程序在现实世界中运行时的输入和输出,并以此为基础编写端到端测试,测试内容越丰富越好。

于是现在,你有了一套测试工具。如果某些测试失败,就先暂时禁用它们。确保代码最终通过测试,即使整个测试套件运行起来需要几个小时。

5

在 README 中写明如何构建和测试应用程序

理想情况下,这是一个用于构建的命令和一个用于测试的命令。一开始如果涉及的内容较多也没关系,在这种情况下,可以把相应的命令放在一个 build.sh 和 test.sh 中,以便封装这些疯狂的过程。

我们的目标是让非 C++ 专家也能编译代码并运行测试,无需向你询问任何问题。在这一步,有些人可能会建议记录项目布局、架构等,但由于下一步将删除大部分内容,我建议不要现在浪费时间,到最后再做。

6

找到容易实现的成果来加快构建和测试

我强调“容易实现的”,也就是不需要更改构建系统、不需要付出巨大努力(我在本文中不断重复这一点,这一点非常重要)。

同样,在一个典型的 C++ 项目中,你会惊讶地发现,构建系统根本不需要做多少工作。试试下面这些方法,看看是否有用:

构建并运行依赖项测试。在一个使用 unittest++ 作为测试框架的项目中(作为一个 CMake 子项目构建),我发现默认行为是每次都构建测试框架的测试并运行它们!这太疯狂了,通常有一个 CMake 变量或类似的东西可以选择不这样做。

构建并运行依赖项的示例程序。和上面的情况一样,这次的罪魁祸首是 mbedtls。同样,设置一个 CMake 变量来选择不进行这项操作就解决了问题。

当你的项目作为另一个父项目的子项目被包含时,默认情况下构建并运行项目的测试。刚才在依赖关系中嘲笑的默认行为,原来我们对其他项目也做了同样的事情!我不是 CMake 专家,但似乎没有在构建中排除测试的标准方法。因此,我建议在默认情况下添加一个名为 MYPROJECT_TEST 的构建变量,默认情况下不设置,只有在设置该变量时才构建和运行测试。通常只有直接参与项目的开发人员才会设置它,示例、生成文档等也是如此。

当你只需要其中一小部分、却要编译所有的第三方依赖程序时:mbedtls 是一个很好的例子,因为它公开了许多编译时标志来切换你可能不需要的部分。小心默认设置,只构建你需要的部分!

为目标列出错误的依赖关系,导致在不需要的情况下重建整个世界:大多数构建系统都有办法从它们的角度输出依赖关系图,这确实有助于搞清这些问题。没有什么比等待数分钟或数小时进行重建更糟糕的了,因为你知道它只应该重建几个文件。

尝试一个更快的链接器:mold 是一个可以直接使用并且在没有成本的情况下真正提供帮助的链接器。然而,这具体也取决于有多少库被链接、整体是否是一个瓶颈等等。

如果可以的话,尝试使用不同的编译器:我曾看到过一些项目,其中 clang 的速度是 gcc 的两倍,而另一些项目则毫无差别。

一旦做到了这一点,还可以再尝试以下几种方法,尽管收益通常要小得多,有时甚至是负的:

LTO(链接时优化):关闭/开启/使用 thin

拆分调试信息

使用 Make 还是 Ninja

使用的文件系统类型,并调整其设置

一旦迭代周期感觉 OK,代码就可以放在显微镜下观察了。如果构建时间很长,那么想要修改代码就不太现实了。
板凳
laocuo1142|  楼主 | 2024-3-6 09:59 | 只看该作者
本帖最后由 laocuo1142 于 2024-3-6 10:00 编辑

7
删除所有不必要的代码

我曾经看到整个代码库的 30% 甚至更多是完全无用的代码,每次进行编译、重构等,你都要为此付出代价。所以,我们要将它们删除。

下面是一些方法:

编译器有一堆 -Wunused-xxx 警告,比如 -Wunused-function。它们可以捕获一些问题,但并不是所有问题都能被捕获。这些警告的每一个实例都应该得到处理,通常只需删除代码,重新构建并重新运行测试即可。在少数情况下,这可能是调用错误函数的表现,因此,我不太愿意将这一步完全自动化。但如果你对你的测试套件很有信心,那就去做吧。

Linters(代码检查工具)可以找到未使用的函数或类字段,比如 cppcheck。根据我的经验,在继承情况下,这些工具会有很多误报,尤其是关于虚拟函数,但好处是这些工具绝对会找到编译器没有注意到的未使用的内容。因此,如果不是用于持续集成(CI),将 linters 添加到你的工具库中是个很好的选择。

我见过更奇特的技术,其中链接器被指示将每个函数放在自己的部分中,在链接时如果检测到某部分未被使用,会删除该部分并打印出来。但这会导致很多噪音,例如标准库函数未被使用,所以我觉得这并不实用。还有人检查生成的程序集,并将其中函数与源代码进行比较,但这对虚拟函数不起作用。所以,根据你的情况,也许值得一试?

还记得支持的平台列表吗?是的,是时候用它来清理所有不支持平台的代码了。在一个专门运行在 FreeBSD 上的项目中支持旧版本的 Solaris 代码?直接删掉;由于我们运行的平台可能没有随机数生成器(当然事实证明并非如此),从而编写了随机数生成器的代码?直接删掉;我们只在现代 Linux 和 macOS 上运行,但其中有针对 POSIX 2001 不受支持情况下的代码?直接删掉;检查主机 CPU 是否支持大端,如果支持就交换字节?直接删掉(你最后一次为大端 CPU 发布代码是什么时候?);多年前为了一个从未实现的假设功能而引入的代码?直接删掉。

做这些事情的好处,不仅在于你将构建时间加快了 5 倍,且没有任何副作用,更重要的是,如果你的老板稍微懂点技术,他们会很喜欢看到删除数千行代码的 PR,你的同事也是如此。

8

Linter

在设置 linter 规则时不要过度,添加一些基本规则,将其融入到开发生命周期中,逐步调整规则并修复出现的问题,然后继续前进。不要试图启用所有规则,那只会收益递减。我过去曾使用过 clang-tidy 和 cppcheck,它们很有帮助,但非常慢且噪音很大,所以要小心。但是,没有 linter 也是不可取的。第一次运行 linter,它会捕捉到很多真实的问题,以至于你会想:为什么即使所有警告都开启了,编译器仍然没有检测到任何问题?

9

代码格式化

等待适当的时机,确保没有分支活动(否则会遇到可怕的合并冲突),随机选择一种代码风格,对整个代码库进行一次性格式化,通常使用 clang-format,提交配置,完成。不要浪费口水去争论实际的代码格式化,它只是为了让差异更小并避免争论,所以不要对此争论!

10

Sanitizers

与 linters 类似,它也可能是一个“兔子洞”。但不幸的是,Sanitizers 绝对是必需的,以便发现真实的、影响生产的、难以检测的 Bug,并能修复它们。-fsanitize=address,undefined 是一个很好的基准,它们通常不会产生误报,因此如果检测到了什么问题,就去修复它。在运行测试时使用它,也能检测到问题,我甚至听说有人在生产环境中启用了一些 sanitizer。所以如果你的性能预算允许的话,这可能是个好主意。

如果你(必须)用来发布生产代码的编译器不支持 sanitizers,你至少可以在开发和运行测试时使用 clang 或类似的编译器。这时,你在构建系统上所做的工作就派上用场了,使用不同的编译器应该相对容易。

有一点可以肯定的是:即使是世界上最好的代码库,即使它拥有最好的编码实践和开发人员,只要你启用了 sanitizers,你绝对会发现一些多年未被发现的可怕 Bug 和内存泄漏。所以去做吧,不过请注意,修复这些问题可能需要大量的工作和重构。

最后一点:理想情况下,所有第三方依赖项在运行测试时也应该启用 sanitizers 进行编译,以便发现其中的问题。

11

添加 CI 管道

正如 Bryan Cantrill 曾经说过的,“我相信大多数固件都是从开发人员笔记本电脑的主目录中产生的”。设置 CI 既快速又免费,还能自动执行我们迄今为止已经设置好的所有功能(linters、代码格式化、测试等)。这样,我们就能在每次更改时,都在一个纯净的环境中生成生产二进制文件。如果作为开发人员的你还没有做到这一点,那么我认为你还没有真正进入21世纪。

锦上添花的是:大多数 CI 系统都允许在不同平台的矩阵上运行这些步骤!这样,你就可以明确检查支持的平台列表是否是真实的,而不仅是理论上的。

通常情况下,CI 管道就像是 make all test lint fmt,所以这并不是什么高深的技术。只需确保工具(linters、sanitizers 等)报告的问题确实未通过管道,否则就不会有人注意到并修复它们。

12

递进式代码改进

这已是众所周知的领域了,我就不多说了。我只想说,很多代码通常都可以大幅简化。

我记得曾经迭代简化过一个复杂的类,它需要手动分配和(有时)释放内存,还要处理通用事务等等。结果发现,这个类所做的一切就是分配一个指针,然后检查指针是否为空,然后…… 就是这样。是的,在我看来,这就是一个布尔值。真/假,没有更多的内容了。

我觉得这一步是最难限制时间的,因为每一轮简化都会开辟新的简化途径。在此,请运用你的最佳判断力,保持保守,并将重点放在安全性、正确性和性能等实际目标上,而不是“代码整洁” 等主观标准上。

根据我的经验,将项目中使用的 C++ 标准升级,有时可帮助简化代码,例如用 for (auto x : items) 循环替换手动增加迭代器的代码。但请记住,这只是达到目的的手段,而不是目的本身。如果你只需要 std::clamp,就自己写吧。

13

用内存安全语言重写?

我现在就在工作中这样做,这值得写一篇单独的文章。但这个过程中也有很多陷阱,只有在有充分理由的情况下,才能这样做。

14

结论

好了,至此这就是一个切实可行、循序渐进的计划,可让你摆脱复杂的遗留 C++ 代码库所带来的麻烦情况。我刚刚在工作中完成了一个项目,现在工作起来轻松多了。我看到以前不愿意接近这个代码库的同事,现在都做出了有意义的贡献,我真的感觉很棒。

还有一些重要的话题我想提一下,但最终没有说,比如在本地调试器中运行代码的绝对必要性、模糊测试、依赖项扫描以查找漏洞等等,也许这些会成为下一篇文章的内容!

15

附录:依赖管理

这一部分非常主观,只是我强烈而偏颇的观点。

迄今为止,我一直小心避免的一个热门话题就是依赖管理。简而言之,在 C++ 中没有依赖管理,大多数人会借助系统包管理器,这一点很容易注意到,因为他们的 README 是这样的:

在 Ubuntu 20.04 上:sudo apt install [100 行软件包]

在 macOS 上:brew install [100 行稍微不同命名的软件包]

其他操作系统:好吧,你可能运气不好了。我猜你得选择一个主流的操作系统并重新安装。

我自己也这么做过,但我认为这是一个糟糕的主意,原因如下:

如上所述,安装说明取决于操作系统和发行版,而更糟糕的是,它们还依赖于发行版的版本。我记得有一个项目花了几个月的时间从 Ubuntu 20.04 迁移到 Ubuntu 22.04,因为它们发布了不同版本的软件包,因此升级发行版也就意味着要同时升级项目的 100 个依赖项。这显然不是个好主意,理想情况下,你应该一次只升级一个依赖项。

总有一些第三方依赖项没有软件包,而你无论如何都得从源代码中构建它。

这些软件包从来都不会按照你想要的标志构建。Fedora 和 Ubuntu 多年来一直在辩论是否启用带有帧指针的软件包(最近才开始启用)。还记得关于 sanitizers 的部分吗?你要如何获取启用了 sanitizer 的依赖项?这是不可能的。还有更多的例子:LTO、-march、调试信息等等。或者它们使用的 C++ 编译器版本与你正在使用的版本不同,从而破坏了两者之间的 C++ ABI。

你希望在审计、开发、调试等过程中,轻松查看当前正在使用的依赖项源代码。

如果遇到 Bug,你希望能轻松地修补依赖项,并且在不必大幅改变构建系统的情况下重新构建。

在不同系统中,你***不会获得完全相同的软件包版本,例如开发人员 Alice 使用的是 macOS,Bob 使用的是 Ubuntu,而生产系统使用的是 FreeBSD。因此,你会遇到无法重现的奇怪差异,这很烦人。

上述观点的推论:你不知道在不同系统上使用的确切版本,因此很难自动生成物料清单(BOM),而这在某些领域是必需的。

这些软件包有时没有你需要的库的版本(静态或动态)。

所以你可能会想,我知道了,我会使用那些新的 C++ 包管理器,比如 Conan、vcpkg 等等……不要那么急:

它们需要外部依赖项,因此你的 CI 会变得更加复杂和缓慢(例如,找出它们需要的 Python 的确切版本,这肯定会与你项目需要的 Python 版本不同)。

它们没有所有版本的软件包。例如:Conan 和 mbedtls,它从版本 2.16.12 跳到版本 2.23.0。中间的版本呢,它们怎么了?它们是否有缺陷,不应该使用吗?谁知道呢!无论如何,安全漏洞也没有列在可用版本中。

它们可能不支持你关心的某些操作系统或架构(FreeBSD、ARM 等)。

我的意思是,如果你的情况适合使用它们,那很好,它绝对是一个比使用系统软件包更好的改进。只是到目前为止,我从未遇到过可以使用它们的项目——总会有一些障碍。

那么我有什么建议呢?好吧,那就老实用 git 子模块和从源代码编译的方法。这的确很麻烦,但也有以下优点:

简单易懂。

比手动管理依赖更好,因为 git 有历史记录和差异功能。

你可以确切地知道,使用的是哪个版本的依赖项,直到提交为止。

升级单个依赖项的版本很简单,只需运行 git checkout。

适用于所有平台。

你可以选择确切的编译标志、编译器等来构建所有的依赖项,甚至可以为每个依赖项量身定制。

即使没有 C++ 经验,开发人员也能轻松掌握。

获取依赖项是安全的,远程源代码在 git 中,没有人会悄悄改变它。

可以递归工作(即:对依赖项的依赖项进行递归构建)。

每个子模块中的每个依赖项的编译,都可以简单到使用 CMake 的 add_subdirectory,或者手动使用 git submodule foreach make。

如果实在无法使用子模块,另一种方法是仍从源代码编译,但通过一个脚本手动执行,获取每个依赖项并进行构建。在实际中的例子:Neovim。

如果你的依赖关系图在 Graphviz 中看起来像一张 Rorschach 测试,并且必须构建数千个依赖项,那就不太容易做到了,但使用像 Buck2 这样的构建系统还是有可能的,它可以进行本地-远程混合构建,并在不同用户的构建之间重用构建产物。

最后,你还可以看看编译语言(Go、Rust 等)的软件包管理器,我知道的所有包管理器都是从源代码开始编译的。除去 git,再加上自动化,这就是同样的方法。
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1296

主题

6230

帖子

15

粉丝
xsh是什么意思 夏天什么花会开 今年30岁属什么生肖 维生素d3是什么 公章一般是什么字体
谦虚什么意思 舌头上有黑苔是什么原因 南笙是什么意思 土豆不能和什么食物一起吃 五月十七是什么星座
红米有什么功效和作用 痛风挂什么科就医 荣五行属什么 蝈蝈是什么动物 珀莱雅适合什么年龄
银手镯发黄是什么原因 嘌呤高会引起什么症状 1958年属什么生肖 甲亢是什么原因导致的 什么是华盖
做蛋糕用什么面粉0297y7.com 甲状腺和甲亢有什么区别hcv8jop6ns9r.cn 总胆红素偏高说明什么hcv8jop6ns3r.cn 大唐集团什么级别hcv9jop4ns5r.cn 夜间睡觉流口水是什么原因hcv9jop3ns4r.cn
婴儿什么时候可以睡枕头hcv8jop0ns3r.cn 男人为什么好色mmeoe.com 鹿茸是鹿的什么部位hcv9jop2ns3r.cn 什么是牙线hcv8jop2ns6r.cn sjb什么意思hcv8jop3ns2r.cn
来大姨妈吃什么对身体好hcv9jop6ns8r.cn 小便白细胞高是什么原因hcv9jop3ns2r.cn 补办港澳通行证需要什么材料hcv9jop1ns5r.cn 手脚脱皮是什么原因导致的hcv9jop8ns3r.cn 失代偿期的肝是属于什么程度hcv9jop5ns0r.cn
慢性活动性胃炎是什么意思hcv9jop3ns8r.cn icloud是什么hcv8jop7ns0r.cn 吃了饭胃胀是什么原因hcv7jop6ns2r.cn 耳石症什么症状hcv9jop2ns5r.cn 真空什么意思hcv8jop9ns4r.cn
百度