写点什么

冗余代码检测与分析

  • 2017-10-09
  • 本文字数:3456 字

    阅读完需:约 11 分钟

本文要点

  • 代码冗余的原因多种多样,从未使用的变量到未完成的变更,再到废弃的代码;
  • 冗余代码会产生一系列的影响,包括源代码臃肿、可靠性及可维护性降低。在某些情况下,死代码也会影响性能;
  • 为了检测冗余代码,作者开发了一个工具,使用 Roslyn 创建 C#源码抽象语法树。作者使用包括 Roslyn 和 MSBuild 在内的多个 GitHub 项目对这个工具进行了训练;
  • 检测到冗余代码可以手动删除或添加注释,也可以使用一种自动化工具。使用特性分支有助于防止冗余代码检入主代码分支。

导言

前段时间,我开发了一款工具,分析源代码中的依赖关系。它使用Roslyn 创建C#源代码抽象语法树使用libclang 创建C++ 源代码抽象语法树。为了验证它是否可以取得预期效果,我接下来实现了识别未使用方法的功能。结果显示,C#代码解析比C++ 代码解析准确得多,因此,我选择把重点放在C#分析器的进一步开发和其他人开发的更复杂的C#代码上。

起初,该工具会标记出冗余方法所在的行,在弄清楚问题范围之后,我实现了自动删除那些行的选项。一个典型的分析过程会多次执行这个工具,尽可能地修剪源代码树。接下来是多个变更还原循环,以便可以成功地构建并通过测试。失败的原因是工具行为异常或者已知的局限性,例如,反射或代码契约。

我选择了多个自己用过而又想回馈的C#项目,用它们的GitHub 库训练了这个工具。最后,我向社区提交了pull request,请求他们讨论我在自己的分支里做的变更。由于这个工具很苛刻,而我又是第一次在网上与人交流,不懂技巧,所以希望我没有冒犯太多的人。在向社区做贡献及参与后续讨论的过程中,我对问题的理解更深入了,本文旨在将我的所得回馈给更广泛的社区。

分析过的GitHub 项目

由JB Evain 编写的 Mono.Cecil 可以将.NET 代码反编译成 C#。根据建议,只有 36 行代码需要删除,经过审核,JB 选择单独添加部分变更,而不是合并分支。

Automatic Graph Layout 是微软官方的一个项目,由 Lev Nachmanson、Sergey Pupyrev、Tim Dwyer、Ted Hart 和 Roman Prutkin 开发,用于绘制图和有向图,Visual Studio 也用它显示各种交互图。Pull request 要求删除 4674 行代码,其中有一些和 SilverLight 有关(已于 2015 年宣布停用)。不经过修改或讨论,分支就被合并了进去。

Roslyn 是一个现代化的 C#编译器,由.NET 基金会的一个团队负责维护。在这个例子下,Pull request 要求删除 18364 行代码,这引发了有益的讨论,并产生了下面讨论的大多数分类。显然,这个分支太大了,无法合并,取而代之,多个单独的议题被提了出来。

MSBuild 是微软官方的一个项目,Visual Studio 的用户应该比较熟悉。根据分析,我提交了删除 3722 行代码的 pull request,遗憾的是,其团队当时没有余力审核我提出的变更建议。

最后分析的是.NET Core 基础库里的System.XML 程序集。这些库由.NET 基金会负责维护,为了删除死代码,其团队发布了一条问题追踪信息。该问题的解决方法是逐个修剪程序集(通常被称为死代码消除),然后比较未修剪程序集和已修剪程序集之间的差异,从而确定哪些编译代码被删除了。通过这些差异可以知道哪些源代码被删除了,这项工作通常是由志愿者社区承担。

鉴于只用该工具抽样分析了五个项目,得出任何结论都是不明智的,尤其是在缺少可靠的统计数据的情况下:

Mono.Cecil MSAGL Roslyn MSBuild CoreFX (System.XML) 已删除 36 4674 18364 3722 427 初次提交时间 2010/04/12 2015/02/22 2014/03/18 2015/03/12 2014/11/08 作者(主要) 39 (1) 25 (4) 285 (31) 90 (6) 526 (29) 鉴于其悠久的历史,MSBuild 的初次提交时间值得注意。仅数一下作者的数量而不评估他们的贡献几乎可以肯定是没有意义的,因此我大致估计了主要的贡献者。说到这里,我推测:

  • 新开发的项目比成熟项目冗余代码多
    • 编写代码时假定将来会需要
    • 测试少,相应的,发现的 Bug 就少
  • 团队越大制造的冗余代码越多
    • 沟通成本会随团队规模的扩大而呈几何级数上升,参见《人月神话

测试结果分类

该工具测试冗余方法,和任何测试一样,分析有成功和失败之分。

真负:

  • 代码有用

假负:

  • 测试代码
    • 测试的功能只有测试使用。这可能是 TDD BDD 过程的产物,在这种情况下,它可能用来追溯不需要或者未满足的需求。
  • 死代码
    • 虽然被调用了,但代码什么都没做,这种调用可以删除。
    • 可以是重复代码
    • 注意,可以认为已废弃代码距离死代码只差一步。

真正:

  • 故意放弃的开发
    • 方法已经添加,但环境变了,这些方法不会再使用了
  • 未完成的变更
    • 代码已变更,有些方法所有引用都删除了,但后续提交时没有将其考虑在内。这种代码有时候称为 oxbow 代码。它可能是重构的意外产物。
  • 未使用的变量
    • 但愿 IDE 会生成编译器警告和 / 或报告。

假正:

  • 工具缺陷
    • 该工具没有检测使用反射代码契约的代码,它依赖构建或测试过程的错误来检测这些代码。
  • 回归
    • 在开发过程中,经过修改,一个方法不再调用标识为死亡的方法,此时,这种情况就会出现。显然,没有测试识别这种回归。
  • 缺少条件编译
    • 通常,这是指缺少#if DEBUG 保护。Roslyn 已经用于 Release 构建,这个过程会识别相应二进制文件中的代码膨胀。
  • 无意放弃的开发
    • 这是在特定的测试数据下发现的,但没有相应的测试。通常来说,我希望这是一个暂时的问题,随着代码增加会自然消失。
  • 在子系统中未使用
    • 分析低效的代码,找出使用该方法的地方。虽然反射也会导致这个问题,但这主要出现在公共方法分析时。一个常见的例子是执行测试辅助功能的时候没有相应的测试代码,不过,那会抛出一个设计问题,定位到测试辅助代码。
  • 仅限调试器使用的方法
    • 在附加了调试器时(可能是 Release 构建),有些项目会有方法打印出状态。这些方法应该以某种方式标记出来,那样就不用分析它们了。

需要注意,工具失败不一定是因为推理不足。其中有些情况是其他工具的处理范畴,如编译器警告应该标记部分问题,重复代码检测器也有用。

冗余代码的影响

假负和真正都可以视为 YAGNI 的实例,这就需要我们注意以下事项:

  • 臃肿的源代码

    • 以前,我遇到过文本编辑器无法打开大文件的情况。
    • 开发人员的认知受限于大脑中可以同时容纳的独立实体的数量,通常认为是 7 个,因此,删除方法可以提升推理能力。
    • 所有开发人员用来阅读和理解源代码的时间很可能都是浪费。
    • 现代 IDE 通常都是从源代码生成抽象语法树,因此,冗余代码降低了它们的速度。
  • 臃肿的可执行文件

    • 在手机上,这尤其是个问题,为了释放存储,升级会删除应用,最终,当功能受到过度损害,就不得不升级设备了。
  • 臃肿的运行时

    • 为黑客提供了更大的攻击面,让他们发现安全问题。
    • 部分面向嵌入式设备的应用程序会在启动时分配所有的动态内存,因此,冗余代码会减少可用堆的大小。
  • 降低性能

    • 在特定的情况下,死代码执行会浪费处理器周期。
  • 降低可靠性

    • 在特定的情况下,死代码执行可能导致崩溃。
  • 降低可维护性

    • 如果执行到冗余代码时停下了,那么行为将不可预测,如 Knight Capital 损失了 4.4 亿美元。
    • 代码覆盖率可能会因假负而升高,因假正而降低。
    • 不必要的测试会降低测试套件的执行速度。
    • 静态分析工具不得不处理更多的代码,并因此效率降低。
    • 软件的设计 / 架构看起来比实际情况复杂。考虑到冗余代码会导致各种各样的问题,最好是将其视为代码异味。

管理已有的冗余代码

处理冗余代码有四种方法:

  • 忽略

    • 虽然会增加维护成本,但至少可以通过编译。
    • 不推荐。
  • 自动从可执行文件中移除

    • 编译型语言在链接器中使用任意一种死代码移除选项移除。
    • 动态语言使用 Tree Shaking 在运行时移除。
    • 也不推荐。
  • 屏蔽

    • 注释掉或者使用预编译指令。
    • 使用 IDE 提供的代码 / 注释折叠避免看到它。
  • 删除

    • 借助源代码控制保留旧版本供参考,可以随时查看。
    • 或者你可能永远不再需要它。

如果要变更源代码,任何变更都需要由熟悉这段代码的人审核,务必保证代码真冗余。

管理新开发项目

假如目标是在开发过程中不引入新的冗余代码,需要制定什么策略?如果允许部分提交,代码库中可能就会增加暂时冗余的代码。这种情况可以通过特性分支来缓解,只有当测试已覆盖且源代码通过了静态分析才合并进主分支。特性分支的另外一个好处就是在放弃开发后可以将分支保持在未合并状态。

关于作者

Zebedee Mason 是一名有着 25 年 CAD/CAM/CAE 行业经验的数值分析员,他最近搬到了 SLAM。多年来,他一直从事与遗留代码库(其中有些在他还是个孩子的时候就已经创建)相关的工作,把许多学术代码改写成商业软件的组件,他甚至还重写了一些原始算法(商业秘密,无法确认)。这种背景使他对软件工具非常了解,他也编写了若干提高生产力的工具,即使以前的开发人员和 IT 管理人员做出了有用的选择。

查看英文原文: Detecting and Analyzing Redundant Code

2017-10-09 18:036431
用户头像

发布了 1008 篇内容, 共 400.2 次阅读, 收获喜欢 345 次。

关注

评论

发布
暂无评论
发现更多内容

华为云GaussDB首席架构师冯柯:摘取皇冠上的明珠,华为云数据库的创新与探索

华为云开发者联盟

数据库 华为云 GaussDB 国产数据库

一篇文章带你彻底了解哈希表

武师叔

算法 哈希表 6月月更

架构实战营模块七作业

天琪实刚亮

中国标准走进国际视野,首个零信任国际标准的诞生往事

脑极体

开始使用DOCKER COMPOSE V2

mengzyou

Docker DevOps Docker-compose

云原生训练营大作业

jjn0703

科创人·神州数码集团CIO沈旸:最佳实践模式正在失灵,开源加速分布式创新

科创人

都有哪些较好用的项目管理软件?

优秀

项目管理 项目管理软件

应用流程挖掘,发现潜在RPA可实施的场景,助力银行优化业务流程

易观分析

RPA

阿里云刘珅孜:云游戏带来的启发——端上创新

阿里云弹性计算

gpu 元宇宙 云游戏

盘点校招面试 HR 可能会问到的问题

宇宙之一粟

面试 校招 6月月更

@全球开发者|首届云原生边缘计算峰会邀您共话

华为云开发者联盟

云原生 边缘计算 kubeedge

企业知识管理体系怎么搭建和运营?

小炮

LabVIEW控制Arduino采集热敏电阻温度数值(基础篇—13)

不脱发的程序猿

单片机 LabVIEW Arduino LIAT 采集热敏电阻温度数值

LabVIEW控制Arduino采集光敏电阻数值(基础篇—14)

不脱发的程序猿

单片机 LabVIEW Arduino LIAT 采集光敏电阻数值

从零开始搭建vue-cli项目

小恺

6月月更

LabVIEW控制Arduino采集LM35温度传感器数值(基础篇—12)

不脱发的程序猿

单片机 LabVIEW Arduino LIAT 采集LM35温度传感器数值

SPDK对接Ceph性能优化

天翼云开发者社区

开发工具

MPLS协议简述

穿过生命散发芬芳

6月月更 MPLS

王者荣耀商城异地多活架构设计

流火

架构实战营模块七 作业

库尔斯

架构实战营

OceanBase 成为信通院首批可信开源社区、可信开源项目

OceanBase 数据库

中国信通院 OceanBase 开源

架构训练 模块七

小马

#架构训练营

当我们进行性能优化,我们在优化什么(LightHouse优化实操)

刘悦的技术博客

性能优化 前端 优化 性能优化手册

动态路由协议之RIP协议,最古老的距离矢量协议!

wljslmz

IP 网络工程师 动态路由 6月月更 路由协议

Docker的安装及日常命令的使用

Geek_982ff5

6月月更

Java27岁了——一次争执引起的Java内卷生涯

写代码两年半

javase Java EE 6月月更

利用Python实现自动操作Excel文件

弑着去忘记う

高性能API网关Kong介绍

天翼云开发者社区

移动平台打造新生态 | 助力企业跨业务、一站式、全场景的系统建设

BeeWorks

后端解构复习(一)

卢卡多多

技术栈 6月月更

冗余代码检测与分析_.NET_Zebedee Mason_InfoQ精选文章