为什么编译器无法完全解决死代码检测?

时间:2015-10-21 18:14:50

标签: compiler-theory

我在C或Java中使用的编译器具有死代码防护功能(当不再执行某行时发出警告)。我的教授说,编译器永远无法完全解决这个问题。我想知道为什么会这样。我不太熟悉编译器的实际编码,因为这是一个基于理论的类。但我想知道他们检查了什么(例如可能的输入字符串与可接受的输入等),以及为什么这不够。

13 个答案:

答案 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只能是偶数或奇数。因此,编译器必须能够理解代码的语义。该如何实施?编译器无法确保永远不会执行最低返回。因此编译器无法检测死代码。