我怎么知道代码中的哪些部分从未使用过?

use*_*898 310 c++ optimization dead-code

我有遗留的C++代码,我应该从中删除未使用的代码.问题是代码库很大.

如何找出从未调用/从未使用过的代码?

Mat*_* M. 196

有两种未使用的代码:

  • 本地的一个,也就是说,在某些函数中,一些路径或变量是未使用的(或者使用但没有任何意义,如写入但从未读过)
  • 全局的:从不调用的函数,从不访问的全局对象

对于第一种,一个好的编译器可以帮助:

  • -Wunused(GCC,Clang)应警告未使用的变量,Clang未使用的分析器甚至已经增加,以警告从未读过的变量(即使使用过).
  • -Wunreachable-code(较旧的海湾合作委员会,2010年被删除)应该警告从未访问的本地区块(它发生在早期返回或总是评估为真的条件)
  • 没有我知道的选项可以警告未使用的catch块,因为编译器通常无法证明不会抛出任何异常.

对于第二种,它要困难得多.静态地,它需要整个程序分析,并且即使链接时间优化实际上可以去除死代码,实际上程序在执行时已经被大量转换,几乎不可能向用户传达有意义的信息.

因此有两种方法:

  • 理论上是使用静态分析仪.一个软件,可以一次性检查整个代码,并找到所有流程.在实践中,我不知道任何可行的方法.
  • 实用的是使用启发式:使用代码覆盖工具(在GNU链中gcov.注意,在编译期间应该传递特定的标志,以使其正常工作).您使用一组良好的输入(单元测试或非回归测试)运行代码覆盖率工具,死代码必须在未到达的代码中...所以您可以从这里开始.

如果您对该主题非常感兴趣,并且有时间和倾向自己实际制定工具,我建议使用Clang库来构建这样的工具.

  1. 使用Clang库获取AST(抽象语法树)
  2. 从入口点开始执行标记和扫描分析

因为Clang将为您解析代码并执行重载解析,所以您不必处理C++语言规则,并且您将能够专注于手头的问题.

但是,这种技术无法识别未使用的虚拟覆盖,因为它们可能被您无法推理的第三方代码调用.

  • 很好,+1.我喜欢你区分可以静态确定在任何情况下都不会运行的代码和不在特定运行中运行的代码,但可能会这样.前者是我认为的重要因素,正如你所说,使用整个程序的AST进行可达性分析是获得它的方法.(防止`foo()`被标记为"被叫",当它只出现在`if(0){foo();}中时,它将是一个奖励,但需要额外的智慧.) (6认同)

小智 34

对于未使用的整个函数(以及未使用的全局变量)的情况,GCC实际上可以为您完成大部分工作,前提是您正在使用GCC和GNU ld.

编译源代码时,使用-ffunction-sections-fdata-sections,然后在链接时使用-Wl,--gc-sections,--print-gc-sections.链接器现在将列出可以删除的所有函数,因为它们从未被调用,并且所有从未引用过的全局变量.

(当然,您也可以跳过该--print-gc-sections部分,让链接器以静默方式删除这些函数,但将它们保留在源代码中.)

注意:这只会找到未使用的完整函数,它不会对函数内的死代码做任何事情.在实时函数中从死代码调用的函数也将被保留.

某些特定于C++的功能也会导致问题,特别是:

  • 虚拟功能.在不知道哪些子类存在以及哪些子类在运行时实际实例化的情况下,您无法知道最终程序中需要存在哪些虚函数.链接器没有足够的信息,所以它必须保持所有这些信息.
  • 具有构造函数的全局变量及其构造函数.通常,链接器不能知道全局的构造函数没有副作用,因此它必须运行它.显然,这意味着全球本身也需要保留.

在这两种情况下,虚拟函数或全局变量构造函数使用的任何东西也必须保持不变.

另一个警告是,如果您正在构建共享库,GCC中的默认设置将导出共享库中的每个函数,从而导致它被"使用",就链接器而言.要解决此问题,您需要将默认值设置为隐藏符号而不是导出(使用eg -fvisibility=hidden),然后显式选择需要导出的导出函数.


Umm*_*mma 25

好吧,如果你使用g ++,你可以使用这个标志 -Wunused

根据文件:

每当变量是未使用的除了其声明中,每当函数声明为static但从定义,每当声明了标号但未使用,每当一个语句的计算是明确不使用的结果.

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

编辑:这是其他有用的标志-Wunreachable-code 根据文件:

此选项旨在警告当编译器检测到至少源代码的整条生产线将不会被执行,因为某些条件是永远不会满足,或者因为它是永远不会返回的过程之后.

更新:我在遗留的C/C++项目中发现了类似的主题死代码检测

  • 这不会捕获从未调用的原型函数的头文件.或者没有被调用的公共类方法.它只能检查是否在该范围内使用了本地范围的变量. (4认同)

Car*_*s V 18

我认为您正在寻找代码覆盖工具.代码覆盖工具将在代码运行时对其进行分析,它将让您知道执行了哪些代码行以及执行了哪些代码行以及哪些代码行未执行.

你可以尝试给这个开源代码覆盖工具一个机会:TestCocoon - C/C++和C#的代码覆盖工具.

  • 这里的关键是"当它正在运行" - 如果你的输入数据没有运用一些代码路径,那么路径将不被识别为使用,是吗? (7认同)

Jus*_*gan 15

这里真正的答案是:你永远不会真正知道.

至少,对于非常重要的案例,你不能确定你已经掌握了所有这些.请阅读Wikipedia关于无法访问代码的文章中的以下内容:

double x = sqrt(2);
if (x > 5)
{
  doStuff();
}
Run Code Online (Sandbox Code Playgroud)

正如维基百科正确指出的那样,一个聪明的编译器可能能够捕获这样的东西.但考虑修改:

int y;
cin >> y;
double x = sqrt((double)y);

if (x != 0 && x < 1)
{
  doStuff();
}
Run Code Online (Sandbox Code Playgroud)

编译器会抓住这个吗?也许.但要做到这一点,它需要做的不仅仅是sqrt针对恒定的标量值.它必须弄清楚(double)y总是一个整数(简单),然后理解sqrt整数集(硬)的数学范围.一个非常复杂的编译器可能能够为sqrt函数或math.h中的每个函数执行此操作,或者为其可以找出其域的任何固定输入函数执行此操作.这变得非常非常复杂,复杂性基本上是无限的.您可以继续为编译器添加复杂的层次,但总会有一种方法可以隐藏某些代码,这些代码对于任何给定的输入集都是无法访问的.

然后有输入集,根本就没有输入.输入在现实生活中没有意义,或被其他地方的验证逻辑阻止.编译器无法知道这些.

最终的结果是,虽然其他人提到的软件工具非常有用,但你永远不会知道你抓住了一切,除非你之后手动完成代码.即便如此,你永远不会确定你没有错过任何东西.

唯一真正的解决方案,恕我直言,要尽可能保持警惕,使用自动化,重构你可以,并不断寻找改进代码的方法.当然,无论如何都要做到这一点是个好主意.

  • 正确并且_不要留下死代码!_如果删除某个功能,请删除死代码。将其留在那里“以防万一”只会导致膨胀(正如您所讨论的)以后很难找到。让版本控制为你做囤积工作。 (2认同)

Mr *_*ark 12

我自己没有使用它,但是cppcheck声称要找到未使用的功能.它可能无法解决完整的问题,但它可能是一个开始.


Ton*_*ony 9

您可以尝试使用Gimple Software的PC-lint/FlexeLint.它声称

在整个项目中查找未使用的宏,typedef,类,成员,声明等

我已经将它用于静态分析并发现它非常好但我不得不承认我没有用它来专门找到死代码.


Sim*_*ter 6

我找到未使用的东西的正常方法是

  1. 确保构建系统正确处理依赖跟踪
  2. 设置第二个监视器,带有全屏终端窗口,运行重复构建并显示第一个屏幕输出。watch "make 2>&1"倾向于在 Unix 上做到这一点。
  3. 在整个源代码树上运行查找和替换操作,在每一行的开头添加“//?”
  4. 通过删除“//?”来修复编译器标记的第一个错误 在相应的行中。
  5. 重复直到没有错误为止。

这是一个有点漫长的过程,但它确实给出了很好的结果。

  • 有优点,但非常劳动密集型。此外,您还必须确保同时取消注释一个函数的所有重载——如果有多个重载适用,取消注释一个不太受欢迎的重载可能会允许编译成功,但会导致错误的程序行为(以及不正确的想法函数使用)。 (2认同)