我一直在使用C或Java编译器进行开发,它们具有死代码检测(当一行代码永远不会被执行时会发出警告)。然而,我的教授表示,这个问题永远无法完全由编译器解决。我想知道为什么会这样。尽管我对编译器的实际编码并不太熟悉,因为这是一门基于理论的课程。但我想知道编译器检查什么(例如可能的输入字符串与可接受的输入等),以及为什么这是不足够的。
我一直在使用C或Java编译器进行开发,它们具有死代码检测(当一行代码永远不会被执行时会发出警告)。然而,我的教授表示,这个问题永远无法完全由编译器解决。我想知道为什么会这样。尽管我对编译器的实际编码并不太熟悉,因为这是一门基于理论的课程。但我想知道编译器检查什么(例如可能的输入字符串与可接受的输入等),以及为什么这是不足够的。
return
后的任何语句。256^(2^64)
个状态是O(1)
,因此死代码检测可以在多项式时间内完成。 - aebabis好的,让我们采用经典的停机问题不可判定证明,并将停机检测器更改为死代码检测器!
C#程序
using System;
using YourVendor.Compiler;
class Program
{
static void Main(string[] args)
{
string quine_text = @"using System;
using YourVendor.Compiler;
class Program
{{
static void Main(string[] args)
{{
string quine_text = @{0}{1}{0};
quine_text = string.Format(quine_text, (char)34, quine_text);
if (YourVendor.Compiler.HasDeadCode(quine_text))
{{
System.Console.WriteLine({0}Dead code!{0});
}}
}}
}}";
quine_text = string.Format(quine_text, (char)34, quine_text);
if (YourVendor.Compiler.HasDeadCode(quine_text))
{
System.Console.WriteLine("Dead code!");
}
}
}
如果YourVendor.Compiler.HasDeadCode(quine_text)
返回false
,那么行System.Console.WriteLine("Dead code!");
将永远不会被执行,因此这个程序实际上确实有死代码,而检测器是错误的。true
,那么行System.Console.WriteLine("Dead code!");
将被执行,由于程序中没有其他代码,所以根本没有死代码,因此检测器再次出错。如果停机问题太难理解,可以这样想。
拿一个被认为对于所有正整数n都成立,但尚未被证明对于每个n都成立的数学问题。一个很好的例子是哥德巴赫猜想,即任何大于二的正偶数都可以表示为两个质数之和。然后(使用适当的bigint库)运行以下程序(伪代码如下):
for (BigInt n = 4; ; n+=2) {
if (!isGoldbachsConjectureTrueFor(n)) {
print("Conjecture is false for at least one value of n\n");
exit(0);
}
}
实现isGoldbachsConjectureTrueFor()
的功能留给读者自己完成,但可以简单地迭代所有小于n
的质数来实现这个目的。
现在,从逻辑上讲,以上内容必须等同于:
for (; ;) {
}
(即无限循环)或print("Conjecture is false for at least one value of n\n");
由于哥德巴赫猜想要么是真的要么不是真的。如果编译器总能消除死代码,那么无论哥德巴赫猜想的真假如何,在这里肯定会有死代码要被消除。然而,为了做到这一点,至少需要解决任意难度的问题。我们可以提供一些证明难度的问题(例如NP完全问题)来确定要消除哪一部分代码。例如,如果我们取这个程序:
String target = "f3c5ac5a63d50099f3b5147cabbbd81e89211513a92e3dcd2565d8c7d302ba9c";
for (BigInt n = 0; n < 2**2048; n++) {
String s = n.toString();
if (sha256(s).equals(target)) {
print("Found SHA value\n");
exit(0);
}
}
print("Not found SHA value\n");
我们知道这个程序要么会打印出"Found SHA value",要么会打印出"Not found SHA value"(如果你能告诉我哪一个是正确的,我会给你加分)。然而,要让编译器能够合理地优化这个过程需要大约2^2048次迭代。实际上,如果没有优化,我预测上述程序可能会一直运行到宇宙热死亡而不打印任何内容,这将是一个很大的优化。sha256
返回一个字节数组,而字节数组在您的语言中不能与字符串相等。 - user253751我不知道C++或Java是否有Eval
类型的函数,但许多语言允许您按名称调用方法。考虑以下(人为制造的)VBA示例。
Dim methodName As String
If foo Then
methodName = "Bar"
Else
methodName = "Qux"
End If
Application.Run(methodName)
在运行时,我们无法确定要调用的方法名称。因此,根据定义,编译器无法绝对确定某个特定的方法是否被调用。
实际上,以按名称调用方法的示例为例,分支逻辑甚至不是必要的。只需说
Application.Run("Bar")
超出编译器的能力。当编译代码时,编译器只知道某个字符串值被传递给该方法。它在运行时之前不会检查该方法是否存在。如果该方法没有在其他地方被调用,通过更正常的方式,尝试查找无用的方法可能会返回错误的结果。任何允许通过反射调用代码的语言都存在相同的问题。
一个简单的例子:
int readValueFromPort(const unsigned int portNum);
int x = readValueFromPort(0x100); // just an example, nothing meaningful
if (x < 2)
{
std::cout << "Hey! X < 2" << std::endl;
}
else
{
std::cout << "X is too big!" << std::endl;
}
现在假设端口0x100设计为只返回0或1。在这种情况下,编译器无法确定else
块永远不会被执行。
然而,在这个基本示例中:
bool boolVal = /*anything boolean*/;
if (boolVal)
{
// Do A
}
else if (!boolVal)
{
// Do B
}
else
{
// Do C
}
编译器可以计算出else
块是死代码。当编译器有足够的数据来判断死代码,并且知道如何应用这些数据来判断给定的块是否为死代码时,它才能警告有关死代码。
编辑
有时在编译时数据并不可用:
// File a.cpp
bool boolMethod();
bool boolVal = boolMethod();
if (boolVal)
{
// Do A
}
else
{
// Do B
}
//............
// File b.cpp
bool boolMethod()
{
return true;
}
在编译 a.cpp 时,编译器无法知道 boolMethod
始终返回 true
。
cout
而不是 count
。 - Alex Lop.高级编译器可以检测并删除无条件的死代码。
但还有一种条件死代码。这种代码在编译时无法确定,只能在运行时检测到。例如,软件可能可配置以包含或排除某些特性,具体取决于用户偏好,使得某些代码段在特定情况下看似死代码。那不是真正的死代码。
有特定的工具可以进行测试、解决依赖关系、删除条件死代码并在运行时重新组合有用的代码以提高效率。这被称为动态死代码消除。但是,如您所见,这超出了编译器的范围。
if (my_func()) {
am_i_dead();
}
my_func()
可以包含任意代码,为了使编译器能够确定它返回 true 还是 false,它要么必须运行该代码,要么执行与运行该代码功能等效的操作。
编译器的理念在于它只对代码执行部分分析,从而简化了独立运行环境的工作。如果执行完整的分析,那么它就不再是一个编译器了。
如果您将编译器视为函数 c()
,其中 c(源代码)=编译好的代码
,将运行环境视为函数 r()
,其中 r(编译好的代码)=程序输出
,那么为了确定任何源代码的输出,您必须计算 r(c(源代码))
值。如果计算 c()
需要知道任何输入的 r(c())
值,那么就不需要单独的 r()
和 c()
:您可以从 c()
推导出一个函数 i()
,使得 i(源代码)=程序输出
。
拿一个函数举例
void DoSomeAction(int actnumber)
{
switch(actnumber)
{
case 1: Action1(); break;
case 2: Action2(); break;
case 3: Action3(); break;
}
}
你能证明actnumber
永远不会是2
,这样Action2()
就永远不会被调用吗?
Action2()
永远不会被调用"的证明,实践中也不可能证明该声明- 编译器无法完全解决。 这种差异就像“存在一个数字X”与“我们可以用十进制写下数字X”。 对于某些X,后者将永远不会发生,尽管前者是真实的。 - CiaPanactnumber==2
,而这个回答仅声称很难,甚至没有说明复杂度。 - MSalters