我如何知道代码中的哪些部分从未使用过?
Posted
技术标签:
【中文标题】我如何知道代码中的哪些部分从未使用过?【英文标题】:How can I know which parts in the code are never used? 【发布时间】:2011-06-16 09:12:18 【问题描述】:我有遗留的 C++ 代码,我应该从中删除未使用的代码。问题是代码库很大。
如何找出从未调用/从未使用过的代码?
【问题讨论】:
我认为代码查询语言可以让您更好地了解整个项目。我不确定 c++ 世界,但似乎有 cppdepend.com(这不是免费的),看起来足够体面。可能是这样的东西可能是免费的。另一件事是,在进行任何形式的重构之前,如果你现在没有单元测试,那么明智的做法就是进行单元测试。通过单元测试,你可以做的是让你的代码覆盖工具分析你的代码,如果你不能覆盖该代码,它本身将有助于删除死代码。 在此处查看参考:en.wikipedia.org/wiki/Unreachable_code 我发现了类似的话题。 ***.com/questions/229069/… 是的,C++ 的有趣之处之一是删除“未使用”的函数仍可能改变程序的结果。 @MSalters:这是一个有趣的问题......为此,我们必须讨论为给定调用选择重载集中的哪个函数,对吗?据我所知,如果有 2 个函数都名为f()
,并且对 f()
的调用明确地解析为第一个,那么仅通过添加名为 @987654326 的第三个函数就不可能使该调用解析为第二个@ - 通过添加第三个功能是“你能做的最糟糕的事情”是导致调用变得模棱两可,从而阻止程序编译。很想看到一个反例(= 被吓坏了)。
【参考方案1】:
GNU 链接器有一个--cref
选项,可以生成交叉引用信息。您可以通过-Wl,--cref
从gcc
命令行传递它。
例如,假设foo.o
定义了一个符号foo_sym
,它也在bar.o
中使用。然后在输出中你会看到:
foo_sym foo.o
bar.o
如果foo_sym
仅限于foo.o
,那么您将看不到任何其他目标文件;后面会跟着另一个符号:
foo_sym foo.o
force_flag options.o
现在,我们不知道foo_sym
没有被使用。它只是一个候选者:我们知道它在一个文件中定义,而没有在任何其他文件中使用。 foo_sym
可以在 foo.o
中定义并在那里使用。
那么,你如何处理这些信息
-
做一些文本修改来识别这些仅限于一个目标文件的符号,生成一个候选列表。
进入源代码,并为每个候选人提供与
static
的内部链接,就像它应该有的那样。
重新编译源代码。
现在,对于那些真正未使用的符号,编译器将能够发出警告,为您精确定位它们;你可以删除那些。
当然,我忽略了其中一些符号被故意使用的可能性,因为它们是为动态链接而导出的(即使链接了可执行文件也可能是这种情况);这是一个更微妙的情况,您必须了解并明智地处理。
【讨论】:
【参考方案2】:是否调用某个函数的一般问题是 NP-Complete。您无法以一般方式提前知道是否会调用某个函数,因为您不知道图灵机是否会停止。如果有一些路径(静态)从 main() 到您编写的函数,您可以获得,但这并不保证您会调用它。
【讨论】:
【参考方案3】:我今天有个朋友问我这个问题,我环顾了一些有前途的 Clang 发展,例如。 ASTMatchers 和 Static Analyzer 在编译过程中可能有足够的可见性以确定死代码部分,但后来我发现了这个:
https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables
这几乎是对如何使用一些似乎是为识别未引用符号而设计的 GCC 标志的完整描述!
【讨论】:
【参考方案4】: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]
【讨论】:
更新:3.1 版本中增加了对 Linux 的 64 位支持。【参考方案5】:真正的答案是:你永远无法确定。
至少,对于非平凡的案例,你不能确定你已经掌握了所有这些。考虑来自Wikipedia's article on unreachable code 的以下内容:
double x = sqrt(2);
if (x > 5)
doStuff();
正如 Wikipedia 正确指出的那样,聪明的编译器可能能够捕捉到这样的东西。但考虑修改:
int y;
cin >> y;
double x = sqrt((double)y);
if (x != 0 && x < 1)
doStuff();
编译器会捕捉到这个吗?可能是。但要做到这一点,它需要做的不仅仅是针对一个恒定的标量值运行sqrt
。它必须弄清楚(double)y
将始终是一个整数(简单),然后理解sqrt
的整数集合的数学范围(困难)。一个非常复杂的编译器可能能够为sqrt
函数、math.h 中的每个函数或任何它可以计算其域的固定输入函数执行此操作。这变得非常非常复杂,而且复杂性基本上是无限的。您可以继续向编译器添加复杂的层次,但总会有一种方法可以潜入一些对于任何给定输入集都无法访问的代码。
还有一些输入集永远不会被输入。输入在现实生活中毫无意义,或者被其他地方的验证逻辑阻塞。编译器无法知道这些。
这样做的最终结果是,虽然其他人提到的软件工具非常有用,但除非您事后手动检查代码,否则您永远无法确定自己是否掌握了所有内容。即便如此,你也永远无法确定自己没有错过任何东西。
恕我直言,唯一真正的解决方案是尽可能保持警惕,尽可能使用自动化,尽可能重构,并不断寻找改进代码的方法。当然,无论如何,这样做是个好主意。
【讨论】:
正确并且不要留下死代码!如果你删除了一个特性,杀死死代码。把它留在那里“以防万一”只会导致膨胀(正如你所讨论的那样),以后很难找到。让版本控制为你做囤积。【参考方案6】:有两种未使用的代码:
本地路径,即在某些函数中,某些路径或变量未使用(或已使用但没有任何意义,例如已写入但从未读取) 全局的:从不调用的函数,从不访问的全局对象对于第一种,一个好的编译器可以提供帮助:
-Wunused
(GCC, Clang) 应该警告未使用的变量,Clang 未使用的分析器甚至已经增加以警告从未读取过的变量(即使使用过)。
-Wunreachable-code
(旧 GCC,removed in 2010)应该警告从未访问过的本地块(它发生在早期返回或总是评估为真的条件)
我知道没有选项可以警告未使用的 catch
块,因为编译器通常无法证明不会引发异常。
对于第二种,难度要大得多。从静态上讲,它需要对整个程序进行分析,即使链接时间优化实际上可能会删除死代码,但实际上程序在执行时已经发生了很大的变化,几乎不可能向用户传达有意义的信息。
因此有两种方法:
理论上是使用静态分析仪。一个软件,可以一次非常详细地检查整个代码并找到所有的流程路径。实际上,我不知道有什么方法可以在这里工作。 实用的方法是使用启发式:使用代码覆盖工具(在 GNU 链中它是gcov
。请注意,在编译期间应传递特定标志以使其正常工作)。您使用一组良好的不同输入(您的单元测试或非回归测试)运行代码覆盖工具,死代码必然在未到达的代码中......因此您可以从这里开始。
如果您对该主题非常感兴趣,并且有时间和意愿自己实际开发出一个工具,我建议您使用 Clang 库来构建这样一个工具。
-
使用 Clang 库获取 AST(抽象语法树)
从入口点开始执行标记和扫描分析
因为 Clang 会为您解析代码并执行重载解析,所以您不必处理 C++ 语言规则,您将能够专注于手头的问题。
但是这种技术无法识别未使用的虚拟覆盖,因为它们可能被您无法推理的第三方代码调用。
【讨论】:
非常好,+1。我喜欢你区分可以静态确定在任何情况下都不会运行的代码和不在特定运行中运行但可能可以运行的代码。我认为前者是重要的,正如你所说,使用整个程序的 AST 进行可达性分析是获得它的方法。 (防止foo()
仅出现在if (0) foo();
中时被标记为“已调用”将是一个好处,但需要额外的聪明才智。)
@j_random_hacker:也许现在我想到使用 CFG(控制流图)会更好(感谢您的示例)。我知道 Clang 热衷于评论你提到的重言式比较,因此使用 CFG 我们可能会在早期发现死代码。
@Matthieu:是的,也许 CFG 也是我的意思,而不是 AST :) 我的意思是:一个有向图,其中顶点是函数,并且从函数 x 到函数 y x 可能会调用 y。 (并且具有重载函数都由不同的顶点表示的重要属性——听起来像 Clang 为你做的那样,唷!)
@j_random_hacker:实际上 CFG 比简单的有向图更复杂,因为它表示要在块中执行的所有代码,并基于条件语句从一个块链接到另一个块。主要优点是它自然适合修剪可以静态确定为死的代码(它会创建可以识别的无法访问的块),因此利用 CFG 比利用 AST 来构建你的有向图更好谈论...我认为:)
@j_random_hacker:实际上 Clang 的 AST 确实如此,它使所有内容都变得明确(或几乎......),因为它用于处理代码,而不仅仅是用于编译它。目前实际上有一个讨论,因为显然初始化列表存在问题,其中这种隐式转换没有出现在 AST 中,但我想它会被修复。【参考方案7】:
我认为您正在寻找code coverage 工具。代码覆盖率工具会在您的代码运行时对其进行分析,并让您知道哪些代码行被执行、执行了多少次,以及哪些没有执行。
您可以尝试给这个开源代码覆盖工具一个机会:TestCocoon - C/C++ 和 C# 的代码覆盖工具。
【讨论】:
这里的关键是“因为它正在运行” - 如果您的输入数据没有使用某些代码路径,那么该路径将不会被识别为已使用,是吗? 没错。如果不运行代码,就无法知道没有到达哪些行。我想知道设置一些单元测试来模拟一些正常运行会有多难。 @drhishch 我认为,大多数此类未使用的代码必须找到链接器而不是编译器。 @drhirsch 没错,编译器可以处理一些无法访问的代码,例如声明但未调用的函数以及一些短路评估,但是取决于用户操作的代码呢,还是运行时变量? @golcarcol 好的,让我们在 a.cpp 中有函数void func()
,它在 b.cpp 中使用。编译器如何检查 func() 是否在程序中使用?这是链接器的工作。【参考方案8】:
这取决于您用于创建应用程序的平台。
例如,如果您使用 Visual Studio,则可以使用 .NET ANTS Profiler 之类的工具,该工具能够解析和分析您的代码。这样,您应该很快知道实际使用了代码的哪一部分。 Eclipse 也有等效的插件。
否则,如果您需要知道最终用户实际使用了您的应用程序的哪些功能,并且如果您可以轻松发布您的应用程序,则可以使用日志文件进行审核。
对于每个主要功能,您都可以跟踪其使用情况,然后在几天/一周后获取该日志文件并查看它。
【讨论】:
.net ANTS Profiler 看起来像是用于 C# 的——你确定它也适用于 C++ 吗? @j_random_hacker :据我所知,它适用于托管代码。所以.net ANTS 肯定无法分析“标准”C++ 代码(即用 gcc 编译,...)。【参考方案9】:我真的没有使用任何工具来做这样的事情......但是,据我在所有答案中看到的,没有人说过这个问题是不可计算的。
这是什么意思?计算机上的任何算法都无法解决这个问题。这个定理(不存在这样的算法)是图灵停机问题的推论。
您将使用的所有工具都不是算法,而是启发式(即不是精确算法)。他们不会为您提供所有未使用的代码。
【讨论】:
我认为 OP 主要想找到没有从任何地方调用的函数,这当然不是不可计算的——大多数现代链接器都可以做到!只需以最少的痛苦和苦差事提取这些信息。 你说得对,我没有看到对主要问题的最后评论。顺便说一句,代码中可能引用了实际上没有使用的函数。这种东西可能检测不到。【参考方案10】:对于未使用的整个函数(和未使用的全局变量),如果您使用的是 GCC 和 GNU ld,GCC 实际上可以为您完成大部分工作。
编译源码时使用-ffunction-sections
和-fdata-sections
,然后链接时使用-Wl,--gc-sections,--print-gc-sections
。链接器现在将列出所有可以删除的函数,因为它们从未被调用过,以及所有从未被引用过的全局变量。
(当然,您也可以跳过--print-gc-sections
部分,让链接器静默删除函数,但将它们保留在源代码中。)
注意:这只会找到未使用的完整函数,它不会对函数中的死代码做任何事情。从实时函数中的死代码调用的函数也将被保留。
一些 C++ 特有的特性也会导致问题,特别是:
虚拟函数。在不知道存在哪些子类以及在运行时实际实例化哪些子类的情况下,您无法知道最终程序中需要存在哪些虚函数。链接器没有足够的相关信息,因此它必须保留所有这些信息。 具有构造函数的全局变量及其构造函数。通常,链接器无法知道全局的构造函数没有副作用,因此它必须运行它。显然,这意味着全局本身也需要保留。在这两种情况下,虚函数或全局变量构造函数使用的任何东西都必须保留。
另外需要注意的是,如果您正在构建共享库,GCC 中的默认设置将导出共享库中的每个函数,从而使其在链接器中被“使用”被关注到。要解决此问题,您需要将默认设置为隐藏符号而不是导出(使用例如-fvisibility=hidden
),然后明确选择需要导出的导出函数。
【讨论】:
非常实用的建议。仅仅获得一个已知不会在任何地方使用的函数列表(即使,正如你所说,这个列表并不完整),我认为会得到很多容易实现的结果。 我认为这些都不适用于未实例化的模板。 这似乎很难产生有用的结果。尤其是“内联”声明的函数和方法会产生很多难以过滤的误报。而且它仍然没有找到在 .cpp 中定义和声明的完全未使用的类,可能是因为它从未被编译到目标文件中。 cppcheck 也没有检测到该结构,任何编译器也没有。 节不是函数。【参考方案11】:如果您使用的是 Linux,您可能需要查看 callgrind
,这是一个 C/C++ 程序分析工具,它是 valgrind
套件的一部分,它还包含检查内存泄漏和其他内存错误的工具(您也应该使用它)。它分析程序的运行实例,并生成有关其调用图以及调用图上节点的性能成本的数据。它通常用于性能分析,但它也会为您的应用程序生成调用图,因此您可以查看调用了哪些函数以及它们的调用者。
这显然是对页面上其他地方提到的静态方法的补充,它只会有助于消除完全未使用的类、方法和函数 - 它并不能帮助在实际调用的方法中找到死代码。
【讨论】:
【参考方案12】:在不导致编译错误的情况下,将尽可能多的公共函数和变量标记为私有或受保护,同时尝试重构代码。通过将函数设为私有并在某种程度上受到保护,您减少了搜索区域,因为私有函数只能从同一个类中调用(除非有愚蠢的宏或其他技巧来规避访问限制,如果是这种情况,我会推荐你找一份新工作)。确定您不需要私有函数要容易得多,因为只有您当前正在处理的类才能调用此函数。如果您的代码库具有小类并且松散耦合,则此方法更容易。如果您的代码库没有小类或耦合非常紧密,我建议先清理它们。
接下来将标记所有剩余的公共函数并制作调用图以找出类之间的关系。从这棵树上,试着找出树枝的哪一部分看起来可以修剪。
这种方法的优点是您可以在每个模块的基础上进行,因此当您的代码库损坏时,很容易保持通过单元测试而无需花费大量时间。
【讨论】:
【参考方案13】:如果你使用 g++,你可以使用这个标志 -Wunused
根据文档:
在变量未使用时发出警告 除了它的声明,每当 一个函数被声明为静态但 从未定义,只要标签是 声明但未使用,并且每当一个 语句计算的结果是 明确未使用。
http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html
编辑:这是其他有用的标志-Wunreachable-code
根据文档:
此选项旨在在编译器检测到至少一整行源代码永远不会执行时发出警告,因为某些条件永远不会满足或因为它是在永远不会返回的过程之后。
更新:我发现了类似的话题Dead code detection in legacy C/C++ project
【讨论】:
这不会捕获那些从未调用过的原型函数的标头。或者不被调用的公共类方法。它只能检查是否在该范围内使用了本地范围的变量。 @Falmarri 我从来没有使用过这个标志。我正试图弄清楚我能用它找到什么样的死代码。-Wunused
警告声明(或一次性声明和定义)但实际上从未使用过的变量。顺便说一句,范围守卫很烦人:p Clang 中有一个实验性实现,它还警告写入但从未读取的非易失性变量(由 Ted Kremenek 编写)。 -Wunreachable-code
警告函数中无法访问的代码,例如,它可以是位于 throw
或 return
语句之后的代码或从未采用的分支中的代码(在重言式比较的情况下会发生这种情况)。 【参考方案14】:
我查找未使用的东西的正常方法是
-
确保构建系统正确处理依赖关系跟踪
设置第二个监视器,带有全屏终端窗口,运行重复构建并显示第一屏输出。
watch "make 2>&1"
倾向于在 Unix 上解决问题。
在整个源代码树上运行查找和替换操作,在每一行的开头添加“//?”
通过删除“//?”修复编译器标记的第一个错误在相应的行中。
重复直到没有错误为止。
这是一个有点漫长的过程,但确实会产生良好的效果。
【讨论】:
有优点,但很费力。此外,您必须确保同时取消注释一个函数的所有重载——如果有多个适用,取消注释一个不太受欢迎的重载可能会使编译成功,但会导致不正确的程序行为(以及不正确的想法)使用函数)。 我只在第一步(所有重载)中取消注释声明,然后在下一次迭代中查看缺少哪些定义;这样,我可以看到实际使用了哪些重载。 @Simon:有趣的是,在对主要问题的评论中,MSalters 指出,即使存在/不存在从未调用过的函数的声明也会影响通过重载找到另外两个函数中的哪一个解决。诚然,这需要非常奇怪和人为的设置,因此在实践中不太可能成为问题。【参考方案15】:如果你使用 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
编辑:这是其他有用的标志 -Wunreachable-code 根据文档:
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.
【讨论】:
当前评分最高的答案中已经提到了这个确切的信息。请阅读现有答案以避免不必要的重复。 现在您可以赢得 Peer Pressure 徽章!【参考方案16】:您可以尝试使用PC-lint/FlexeLint from Gimple Software。它声称
查找未使用的宏,typedef, 类、成员、声明等 贯穿整个项目
我曾经用它做静态分析,发现它非常好,但我不得不承认我没有用它来专门寻找死代码。
【讨论】:
【参考方案17】:我自己没用过,但是cppcheck,声称可以找到未使用的功能。它可能无法解决完整的问题,但它可能是一个开始。
【讨论】:
是的,它能够找到本地未引用的变量和函数。 是的,使用cppcheck --enable=unusedFunction --language=c++ .
来查找这些未使用的函数。【参考方案18】:
我不认为它可以自动完成。
即使使用代码覆盖工具,您也需要提供足够的输入数据才能运行。
可能是非常复杂且价格昂贵的静态分析工具,例如来自 Coverity's 或 LLVM compiler 的静态分析工具可能会有所帮助。
但我不确定,我更喜欢手动代码审查。
更新
嗯.. 只删除未使用的变量,但未使用的函数并不难。
更新
看了其他答案和cmets,我更加坚信做不到。
您必须了解代码才能进行有意义的代码覆盖率测量,并且如果您知道手动编辑会比准备/运行/审查覆盖率结果更快。
【讨论】:
您的回答措辞具有误导性,LLVM 并没有什么贵重之处……它是免费的! 手动编辑不会帮助您处理通过程序中的逻辑分支传递的运行时变量。如果您的代码从未满足某个标准并因此始终遵循相同的路径怎么办?【参考方案19】:一种方法是使用调试器和编译器功能,在编译期间消除未使用的机器代码。
一旦消除了某些机器代码,调试器将不允许您在相应的源代码行上放置断点。因此,您将断点放置在任何地方并启动程序并检查断点 - 处于“没有为此源加载代码”状态的断点对应于已消除的代码 - 要么该代码从未被调用,要么它已被内联,你必须执行一些最低限度分析以找出这两者中的哪一个发生了。
至少它在 Visual Studio 中是这样工作的,我猜其他工具集也可以做到这一点。
这是很多工作,但我想比手动分析所有代码要快。
【讨论】:
我认为 OP 的问题是关于如何找到更小、更易于管理的源代码子集,而不是确保编译后的二进制文件高效。 @j_random_hacker 我试了一下,结果发现代码消除甚至可以用于追踪原始源代码。 您是否必须在 Visual Studio 上使用一些特定的编译器标志才能实现它?它是只在发布模式下工作还是在调试模式下也能工作? 那些被编译器使用但优化出来的行呢? @Naveen:在 Visual C++ 9 中,您必须打开优化并使用 /OPT:ICF以上是关于我如何知道代码中的哪些部分从未使用过?的主要内容,如果未能解决你的问题,请参考以下文章