我在C或Java中使用的编译器具有死代码防护功能(当不再执行某行时发出警告)。我的教授说,编译器永远无法完全解决这个问题。我想知道为什么会这样。我不太熟悉编译器的实际编码,因为这是一个基于理论的类。但我想知道他们检查了什么(例如可能的输入字符串与可接受的输入等),以及为什么这不够。
答案 0 :(得分:273)
死代码问题与Halting problem。
有关Alan Turing证明,编写一个通用算法是不可能的,该算法将被赋予一个程序,并能够决定该程序是否为所有输入停止。您可以为特定类型的程序编写这样的算法,但不能为所有程序编写。
这与死代码有什么关系?
Halting问题可以解决死代码的问题。也就是说,如果您找到一个可以在任何程序中检测死代码的算法,那么您可以使用该算法来测试任何程序是否会停止。由于已经证明这是不可能的,因此编写死代码算法也是不可能的。
如何将死代码算法转换为停止问题的算法?
简单:在要检查暂停的程序结束后添加一行代码。如果您的死码检测器检测到该线路已死,那么您就知道该程序没有停止。如果没有,那么你知道你的程序停止了(到达最后一行,然后到你添加的代码行)。
编译器通常检查在编译时可以证明的事情是否已经死亡。例如,依赖于在编译时可以确定为false的条件的块。或return
之后的任何陈述(在同一范围内)。
这些是特定情况,因此可以为它们编写算法。有可能为更复杂的情况编写算法(比如检查条件在语法上是否是矛盾的算法,因此总是返回false),但仍然不能涵盖所有可能的情况。
答案 1 :(得分:76)
好吧,让我们采用停止问题不可判定性的经典证明,并将停止检测器更改为死码检测器!
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.WriteLn("Dead code!");
将不会被执行,因此该程序实际上 具有死代码和检测器错了。
但是如果它返回true
,那么行System.Console.WriteLn("Dead code!");
将被执行,并且由于程序中没有更多的代码,所以根本没有死代码,所以同样,检测器是错的。
所以你有它,一个死代码检测器只返回“有死代码”或“没有死代码”有时必须产生错误的答案。
答案 2 :(得分:65)
如果暂停问题过于模糊,请以这种方式考虑。
对于所有正整数的 n ,我们认为数学问题是正确的,但对于每个 n ,都没有被证明是正确的。一个很好的例子是Goldbach's conjecture,任何大于2的正整数都可以用两个素数之和来表示。然后(使用适当的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");
我们知道该程序将打印出“找到SHA值”或“未找到SHA值”(如果你能告诉我哪一个是真的,可以获得奖励积分)。但是,对于编译器能够合理地优化,需要大约2 ^ 2048次迭代。它实际上是一个很好的优化,因为我预测上述程序将(或可能)运行直到宇宙的热量死亡,而不是没有优化打印任何东西。
答案 3 :(得分:34)
我不知道C ++或Java是否具有Eval
类型函数,但是许多语言允许您按名称调用方法 。考虑以下(人为的)VBA示例。
Dim methodName As String
If foo Then
methodName = "Bar"
Else
methodName = "Qux"
End If
Application.Run(methodName)
在运行时之前无法知道要调用的方法的名称。因此,根据定义,编译器无法绝对确定地知道永远不会调用特定方法。
实际上,给定按名称调用方法的示例,分支逻辑甚至不是必需的。简单地说
Application.Run("Bar")
超过编译器可以确定的。编译代码时,所有编译器都知道某个字符串值正在传递给该方法。它不会检查该方法是否存在直到运行时。如果该方法不在其他地方调用,通过更常规的方法,尝试查找死方法可能会返回误报。任何允许通过反射调用代码的语言都存在同样的问题。
答案 4 :(得分:12)
一个简单的例子:
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
。
答案 5 :(得分:12)
高级编译器可以检测并删除无条件的死代码。
但也有条件死代码。这是在编译时无法识别的代码,只能在运行时检测到。例如,软件可以被配置为根据用户偏好包括或排除某些特征,使得某些代码段在特定场景中看起来似乎已经死亡。那不是真正的死代码。
有一些特定的工具可以进行测试,解决依赖关系,删除条件死代码并在运行时重新组合有用的代码以提高效率。这称为动态死代码消除。但正如您所看到的那样,它超出了编译器的范围。
答案 6 :(得分:4)
编译器总是缺少一些上下文信息。例如。你可能知道,double值永远不会超过2,因为这是数学函数的一个特性,你可以从库中使用它。编译器甚至看不到库中的代码,它永远不会知道所有数学函数的所有特征,并检测所有已经实现的复杂方法。
答案 7 :(得分:4)
编译器不一定能看到整个程序。我可以有一个调用共享库的程序,该程序在我的程序中调用一个函数,该函数不会被直接调用。
因此,如果该库在运行时被更改,那么对于它编译的库而言,该函数可能会变为活动状态。
答案 8 :(得分:3)
如果编译器可以准确地消除所有死代码,它将被称为解释器。
考虑这个简单的场景:
library(dplyr)
df %>%
group_by(X) %>%
slice(myDate %>%
as.Date("%d.%m.%Y") %>%
which.min %>%
last)
if (my_func()) {
am_i_dead();
}
可以包含任意代码,并且为了让编译器确定它是返回true还是false,它必须运行代码或执行与运行代码功能相同的东西。
编译器的想法是它只对代码执行部分分析,从而简化了单独运行环境的工作。如果执行完整分析,则不再是编译器。
如果您将编译器视为函数my_func()
,其中c()
,运行环境为c(source)=compiled code
,其中r()
,则确定任何源的输出代码,您必须计算r(compiled code)=program output
的值。如果计算r(c(source code))
需要了解c()
对任何输入的值,则无需单独的r(c())
和r()
:您只需派生一个函数{{ 1}}来自c()
,i()
。
答案 9 :(得分:2)
采取功能
void DoSomeAction(int actnumber)
{
switch(actnumber)
{
case 1: Action1(); break;
case 2: Action2(); break;
case 3: Action3(); break;
}
}
您能否证明actnumber
永远不会2
,以致Action2()
永远不会被调用......?
答案 10 :(得分:2)
其他人对停止问题等进行了评论。这些通常适用于部分功能。然而,很难/不可能知道是否使用了整个类型(类/等)。
在.NET / Java / JavaScript和其他运行时驱动的环境中,没有任何停止类型通过反射加载。这在依赖注入框架中很流行,在反序列化或动态模块加载时更难以推理。
编译器无法知道是否会加载此类型。他们的名字可以在运行时来自外部配置文件。
您可能希望搜索树摇动,这是尝试安全删除未使用的代码子图的工具的常用术语。
答案 11 :(得分:1)
我不同意停止问题。我不会把这些代码称为死,即使实际上它永远不会被达到。
相反,我们考虑一下:
for (int N = 3;;N++)
for (int A = 2; A < int.MaxValue; A++)
for (int B = 2; B < int.MaxValue; B++)
{
int Square = Math.Pow(A, N) + Math.Pow(B, N);
float Test = Math.Sqrt(Square);
if (Test == Math.Trunc(Test))
FermatWasWrong();
}
private void FermatWasWrong()
{
Press.Announce("Fermat was wrong!");
Nobel.Claim();
}
(忽略类型和溢出错误)死代码?
答案 12 :(得分:-1)
看看这个例子:
public boolean isEven(int i){
if(i % 2 == 0)
return true;
if(i % 2 == 1)
return false;
return false;
}
编译器无法知道int只能是偶数或奇数。因此,编译器必须能够理解代码的语义。该如何实施?编译器无法确保永远不会执行最低返回。因此编译器无法检测死代码。