我有遗留的c++代码,我应该删除不使用的代码。问题是代码库很大。
我如何才能知道哪些代码从未被调用/从未被使用?
我有遗留的c++代码,我应该删除不使用的代码。问题是代码库很大。
我如何才能知道哪些代码从未被调用/从未被使用?
当前回答
有两种未使用的代码:
局部路径,即在某些函数中,某些路径或变量未使用(或使用但没有任何意义,如写入但从未读取) 全局的:永远不会调用的函数,永远不会访问的全局对象
对于第一种类型,一个好的编译器可以帮助:
-Wunused (GCC, Clang)应该警告未使用的变量,Clang未使用的分析器甚至已经增加到警告从未读取的变量(即使使用)。 -Wunreachable-code(旧的GCC,在2010年被移除)应该警告从未被访问的局部块(它发生在早期返回或条件总是计算为true时) 据我所知,没有选项可以警告未使用的catch块,因为编译器通常不能证明没有异常将被抛出。
对于第二种,要困难得多。静态地,它需要整个程序的分析,即使链接时间优化实际上可以删除死代码,在实践中,程序在执行时已经进行了如此多的转换,以至于几乎不可能向用户传递有意义的信息。
因此有两种方法:
The theoretic one is to use a static analyzer. A piece of software that will examine the whole code at once in great detail and find all the flow paths. In practice I don't know any that would work here. The pragmatic one is to use an heuristic: use a code coverage tool (in the GNU chain it's gcov. Note that specific flags should be passed during compilation for it to work properly). You run the code coverage tool with a good set of varied inputs (your unit-tests or non-regression tests), the dead code is necessarily within the unreached code... and so you can start from here.
如果您对这个主题非常感兴趣,并且有时间和意愿自己开发一个工具,我建议您使用Clang库来构建这样一个工具。
使用Clang库获取AST(抽象语法树) 从入口点开始执行标记-清除分析
因为Clang将为您解析代码,并执行重载解析,所以您不必处理c++语言规则,并且您将能够集中精力处理手头的问题。
然而,这种技术不能识别未使用的虚拟覆盖,因为它们可能由您无法推理的第三方代码调用。
其他回答
我真的没有使用过任何工具做这样的事情…但是,就我所看到的所有答案,没有人说过这个问题是不可计算的。
这是什么意思呢?这个问题不能用计算机上的任何算法解决。这个定理(这样的算法不存在)是图灵停止问题的一个推论。
你将使用的所有工具都不是算法,而是启发式(即不是精确的算法)。他们不会给你所有没有使用的代码。
对于未使用的整个函数(和未使用的全局变量),GCC实际上可以为您完成大部分工作,前提是您使用GCC和GNU ld。
编译源代码时,使用- function-sections和-fdata-sections,然后链接时使用-Wl,——gc-sections,——print-gc-sections。链接器现在将列出所有可以删除的函数,因为它们从未被调用,以及所有从未被引用的全局函数。
(当然,你也可以跳过——print-gc-sections部分,让链接器无声地删除函数,但将它们保留在源代码中。)
注意:这只会发现未使用的完整函数,它不会对函数中的死代码做任何事情。在活函数中从死代码调用的函数也将被保留。
一些c++特有的特性也会导致问题,特别是:
Virtual functions. Without knowing which subclasses exist and which are actually instantiated at run time, you can't know which virtual functions you need to exist in the final program. The linker doesn't have enough information about that so it will have to keep all of them around. Globals with constructors, and their constructors. In general, the linker can't know that the constructor for a global doesn't have side effects, so it must run it. Obviously this means the global itself also needs to be kept.
在这两种情况下,虚函数或全局变量构造函数使用的任何东西都必须保留。
另外需要注意的是,如果您正在构建一个共享库,GCC中的默认设置将导出共享库中的每个函数,导致只要链接器就会“使用”它。为了解决这个问题,你需要将默认值设置为隐藏符号而不是导出(例如使用-fvisibility=hidden),然后显式地选择你需要导出的导出函数。
Mark as much public functions and variables as private or protected without causing compilation error, while doing this, try to also refactor the code. By making functions private and to some extent protected, you reduced your search area since private functions can only be called from the same class (unless there are stupid macro or other tricks to circumvent access restriction, and if that's the case I'd recommend you find a new job). It is much easier to determine that you don't need a private function since only the class you're currently working on can call this function. This method is easier if your code base have small classes and is loosely coupled. If your code base does not have small classes or have very tight coupling, I suggest cleaning those up first.
接下来将标记所有剩余的公共函数,并制作一个调用图,以找出类之间的关系。从这棵树上,试着找出树枝的哪一部分看起来可以修剪。
这种方法的优点是你可以在每个模块的基础上进行测试,所以当你的代码库损坏时,你很容易通过单元测试,而不会有很长一段时间。
如果某个函数将被调用的一般问题是np完全的。一般来说,你无法提前知道某个函数是否会被调用,就像你不知道图灵机是否会停止一样。如果存在从main()到您所编写的函数的某个路径(静态),则可以获取,但这并不保证它将被调用。
我有个朋友今天问了我这个问题,我看了看一些有前途的Clang开发,例如ASTMatchers和Static Analyzer,它们可能在编译过程中有足够的可见性来确定死代码部分,但后来我发现了这个:
https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables
它几乎完整地描述了如何使用一些GCC标志,这些标志似乎是为了识别未引用的符号而设计的!