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

时间:2011-01-27 07:59:27

标签: c++ optimization dead-code

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

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

18 个答案:

答案 0 :(得分:194)

有两种未使用的代码:

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

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

  • -Wunused(GCC,Clang)应该警告未使用的变量,Clang未使用的分析器甚至已经增加,以警告从未读过的变量(即使使用过)。
  • -Wunreachable-code(较旧的GCC,removed in 2010)应该警告从未访问的本地块(它发生在早期返回或始终评估为真的条件下)
  • 我没有选择警告未使用的catch块,因为编译器通常无法证明不会抛出任何异常。

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

因此有两种方法:

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

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

  1. 使用Clang库获取AST(抽象语法树)
  2. 从入口点开始进行标记和扫描分析
  3. 因为Clang将为您解析代码并执行重载解析,所以您不必处理C ++语言规则,并且您将能够专注于手头的问题。

    然而,这种技术无法识别未使用的虚拟覆盖,因为它们可能被您无法解释的第三方代码调用。

答案 1 :(得分:34)

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

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

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

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

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

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

在这两种情况下,虚拟函数或全局变量构造函数使用的任何内容也必须保留。

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

答案 2 :(得分:25)

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

根据文件:

  

每当变量未使用时发出警告   除了宣言,无论何时   函数声明为static但是   从未定义,只要标签是   声明但未使用,并且每当a   语句计算结果   明确没有使用。

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

修改:这是其他有用的标记-Wunreachable-code 根据文件:

  

此选项用于在编译器检测到至少整行源代码将永远不会被执行时发出警告,因为某些条件永远不会得到满足,或者因为它位于永不返回的过程之后。

更新:我发现了类似主题Dead code detection in legacy C/C++ project

答案 3 :(得分:18)

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

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

答案 4 :(得分:15)

这里真正的答案是:你永远无法确定。

至少,对于非常重要的案例,你不能确定你已经掌握了所有这些。请考虑Wikipedia's article on unreachable code中的以下内容:

double x = sqrt(2);
if (x > 5)
{
  doStuff();
}

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

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

if (x != 0 && x < 1)
{
  doStuff();
}

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

然后有输入集只是永远不会进入。输入在现实生活中没有任何意义,或被其他地方的验证逻辑阻止。编译器无法知道这些。

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

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

答案 5 :(得分:12)

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

答案 6 :(得分:9)

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

  

找到未使用的宏,typedef's,   课程,成员,声明等   贯穿整个项目

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

答案 7 :(得分:4)

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

  1. 确保构建系统正确处理依赖关系跟踪
  2. 设置第二台显示器,带有全屏终端窗口,运行重复构建并显示第一屏输出。 watch "make 2>&1"倾向于在Unix上做到这一点。
  3. 在整个源树上运行查找和替换操作,在每行的开头添加“//?”
  4. 修复编译器标记的第一个错误,删除“//?”在相应的行中。
  5. 重复,直到没有错误。
  6. 这是一个有点冗长的过程,但它确实给出了很好的结果。

答案 8 :(得分:4)

将公共函数和变量标记为private或protected而不会导致编译错误,在执行此操作时,请尝试重构代码。通过使函数私有并在某种程度上受到保护,您减少了搜索区域,因为私有函数只能从同一个类调用(除非有愚蠢的宏或其他技巧来规避访问限制,如果是这种情况我建议你找一份新工作)。确定您不需要私有函数要容易得多,因为只有您当前正在处理的类才能调用此函数。如果您的代码库具有较小的类并且松散耦合,则此方法更容易。如果你的代码库没有小类或者耦合非常紧密,我建议先清理它们。

接下来将标记所有剩余的公共函数,并创建一个调用图来确定类之间的关系。从这棵树中,尝试找出分支的哪个部分看起来可以修剪。

这种方法的优点是你可以在每个模块的基础上完成它,因此很容易在没有很长一段时间的情况下继续通过你的单元测试。

答案 9 :(得分:3)

我真的没有使用任何做这种事情的工具......但是,就我在所有答案中看到的情况而言,没有人曾经说过这个问题是无法计算的。

这是什么意思?这个问题无法通过计算机上的任何算法解决。这个定理(这种算法不存在)是图灵停止问题的必然结果。

您将使用的所有工具都不是算法,而是启发式(即不是精确的算法)。他们不会完全给你所有未使用的代码。

答案 10 :(得分:3)

如果您使用的是Linux,则可能需要查看callgrind,这是一个C / C ++程序分析工具,它是valgrind套件的一部分,它还包含检查内存泄漏的工具。其他内存错误(您应该使用它)。它分析程序的运行实例,并生成有关其调用图的数据,以及调用图上节点的性能成本。它通常用于性能分析,但它也为您的应用程序生成一个调用图,因此您可以看到调用的函数以及调用者。

这显然是对页面上其他地方提到的静态方法的补充,它只对消除完全未使用的类,方法和函数有帮助 - 它无助于在实际调用的方法中找到死代码。

答案 11 :(得分:2)

一种方法是使用调试器和编译器功能,在编译期间消除未使用的机器代码。

一旦取消了某些机器代码,调试器就不会让你在相应的源代码行上放置一个breakpojnt。所以你在任何地方放置断点并启动程序并检查断点 - 那些“没有为此源加载的代码”状态对应于被删除的代码 - 要么代码从未被调用,要么已被内联,你必须执行一些最小值分析找出这两者中的哪一个发生了。

至少它是如何在Visual Studio中工作的,我猜其他工具集也可以做到这一点。

这是很多工作,但我想比手动分析所有代码更快。

答案 12 :(得分:1)

这取决于您用于创建应用程序的平台。

例如,如果您使用Visual Studio,则可以使用.NET ANTS Profiler之类的工具来解析和分析您的代码。这样,您应该快速了解代码的哪个部分实际使用。 Eclipse也有相同的插件。

否则,如果您需要了解最终用户实际使用的应用程序功能,并且您可以轻松地发布应用程序,则可以使用日志文件进行审核。

对于每个主要功能,您可以跟踪其使用情况,并在几周/周后获取该日志文件,并查看它。

答案 13 :(得分:1)

CppDepend是一种商业工具,可以检测未使用的类型,方法和字段,并可以执行更多操作。它适用于Windows和Linux(但目前没有64位支持),并提供为期2周的试用版。

免责声明:我不在那里工作,但我拥有此工具的许可证(以及NDepend,这是.NET代码的更强大的替代方案。)

对于那些好奇的人,这里有一个用于检测死方法的示例内置(可自定义)规则,用CQLinq编写:

// <Name>Potentially dead Methods</Name>
warnif count > 0
// Filter procedure for methods that should'nt be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
    m => !m.IsPublic &&       // Public methods might be used by client applications of your Projects.
         !m.IsEntryPoint &&            // Main() method is not used by-design.
         !m.IsClassConstructor &&      
         !m.IsVirtual &&               // Only check for non virtual method that are not seen as used in IL.
         !(m.IsConstructor &&          // Don't take account of protected ctor that might be call by a derived ctors.
           m.IsProtected) &&
         !m.IsGeneratedByCompiler
)

// Get methods unused
let methodsUnused = 
   from m in JustMyCode.Methods where 
   m.NbMethodsCallingMe == 0 && 
   canMethodBeConsideredAsDeadProc(m)
   select m

// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
   methods => // Unique loop, just to let a chance to build the hashset.
              from o in new[] { new object() }
              // Use a hashet to make Intersect calls much faster!
              let hashset = methods.ToHashSet()
              from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
              where canMethodBeConsideredAsDeadProc(m) &&
                    // Select methods called only by methods already considered as dead
                    hashset.Intersect(m.MethodsCallingMe).Count() == m.NbMethodsCallingMe
              select m)

from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
select new { m, m.MethodsCallingMe, depth = deadMethodsMetric[m] }

答案 14 :(得分:0)

我认为不能自动完成。

即使使用代码覆盖工具,您也需要提供足够的输入数据才能运行。

可能非常复杂且价格高昂的静态分析工具(如Coverity'sLLVM compiler)可能会有所帮助。

但我不确定,我更喜欢手动代码审查。

<强>已更新

嗯..只删除未使用的变量,但未使用的函数并不难。

<强>已更新

在阅读了其他答案和评论之后,我更加坚信它无法完成。

您必须知道代码才能获得有意义的代码覆盖率,如果您知道手动编辑的速度比准备/运行/审核覆盖率结果要快。

答案 15 :(得分:0)

我今天有一个朋友问我这个问题,我环顾了一些有希望的Clang开发项目,例如: ASTMatcherStatic Analyzer在编译过程中可能具有足够的可见性,以确定死代码部分,但后来我发现了这一点:

https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables

它几乎是一个完整的描述,说明如何使用一些看似为识别未引用符号而设计的GCC标志!

答案 16 :(得分:0)

如果调用某个函数的一般问题是NP-Complete。如果您不知道图灵机是否会停止,您将无法事先知道是否会调用某些功能。如果有一些路径(静态地)从main()到你编写的函数,你可以得到,但这并不能保证你将被调用。

答案 17 :(得分:-3)

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

根据文件:

Warn whenever a variable is unused aside from its declaration, whenever a function is declared static but never defined, whenever a label is declared but not used, and whenever a statement computes a result that is explicitly not used.

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

编辑:这是其他有用的标志 - 无法访问的代码根据文档:

This option is intended to warn when the compiler detects that at least a whole line of source code will never be executed, because some condition is never satisfied or because it is after a procedure that never returns.