证明编译器无法检测到无效代码


32

我打算讲授有关各种主题的冬季课程,其中之一将是编译器。现在,我在考虑整个季度要分配的作业时遇到了这个问题,但是这让我很困惑,因此我可以用它作为示例。

public class DeadCode {
  public static void main(String[] args) {
     return;
     System.out.println("This line won't print.");
  }
}

在上面的程序中,很明显,由于,print语句将永远不会执行return。编译器有时会给出有关死代码的警告或错误。例如,以上代码将无法在Java中编译。但是,javac编译器不会在每个程序中检测到所有死代码实例。我如何证明没有编译器可以这样做?


29
您的背景是什么,教学的背景是什么?直言不讳,我有点担心您必须问这个问题,因为您将要教书。但是好电话问这里!
拉斐尔


9
@MichaelKjörling即使没有这些考虑,也无法进行死代码检测。
David Richerby 2015年

2
BigInteger i = 0; while(isCollatzConjectureTrueFor(i)) i++; printf("Hello world\n");
user253751

2
@immibis这个问题要求证明不可能进行死代码检测。您提供了一个示例,其中正确的死代码检测需要解决数学中的一个开放问题。那不能证明死代码检测是不可能的
David Richerby

Answers:


57

这一切都来自于暂停问题的不确定性。假设我们有一个“完美”的死代码函数,一些Turing Machine M和一些输入字符串x,以及一个类似于以下内容的过程:

Run M on input x;
print "Finished running input";

如果M永远运行,则我们将删除打印语句,因为我们将永远无法到达它。如果M不会永远运行,那么我们需要保留print语句。因此,如果我们有一个死代码去除器,它也使我们能够解决暂停问题,因此我们知道不可能有这样的死代码去除器。

我们通过“保守近似”来解决这个问题。因此,在上面的图灵机示例中,我们可以假设在x上运行M可能会完成,因此我们可以放心地使用它并且不要删除print语句。在您的示例中,我们知道无论哪个函数停止运行或停止,我们都无法到达该打印语句。

通常,这是通过构造“控制流图”来完成的。我们做一些简化的假设,例如“ while循环的结尾连接到开头,而后面的语句连接”,即使它永远运行或只运行一次并且不访问两者。同样,我们假设if语句可以到达其所有分支,即使实际上从未使用过。这些简化使我们能够像您给出的示例一样删除“明显无效的代码”,同时仍可确定。

为了澄清评论中的一些混淆:

  1. Nitpick:对于固定M,这始终是可决定的。M必须是输入

    正如拉斐尔所说,在我的示例中,我们将图灵机视为输入。这个想法是,如果我们有一个完美的DCE算法,我们将能够构建我为任何Turing Machine提供的代码片段,而拥有DCE可以解决暂停问题。

  2. 不服气。在无分支的直接执行中作为平淡的语句返回并不难决定。(我的编译器告诉我,它能够解决这个问题)

    对于njzk2提出的问题:您是完全正确的,在这种情况下,您可以确定在返回之后无法声明。这是因为它很简单,我们可以使用控制流图约束来描述它的不可达性(即return语句中没有传出的边)。但是没有完美的死代码消除器,可以消除所有未使用的代码。

  3. 我不接受依赖于输入的证明。如果存在这样的用户输入,可以使代码有限,那么对于编译器来说,假设随后的分支没有死是正确的。我看不到所有这些支持是什么,这很明显(例如,无尽的标准输入),而且是错误的。

    对于TomášZato:这实际上不是依赖输入的证明。相反,将其解释为“全部”。它的工作原理如下:假设我们有一个完善的DCE算法。如果您给我一个任意的Turing Machine M并输入x,我可以使用DCE算法来确定M是否暂停,方法是构造上面的代码片段,然后查看打印语句是否被删除。这种在参数和参数上任意证明连续状态的技术在数学和逻辑中很常见。

    我不完全理解TomášZato关于代码有限的观点。当然,代码是有限的,但是完美的DCE算法必须适用于所有代码,这是无限的集合。同样,虽然代码本身是有限的,但潜在的输入集是无限的,代码的潜在运行时间也是如此。

    至于考虑最后分支是不死的:就我所谈论的“保守近似”而言,这是安全的,但仅检测OP所要求的所有无效代码实例是不够的。

考虑如下代码:

while (true)
  print "Hello"
print "goodbye"

显然,我们可以删除print "goodbye"而不更改程序的行为。因此,它是无效代码。但是,如果有一个不同的函数调用,而不是(true)while状态,那么我们不知道,如果我们可以将其删除或不,导致不可判定性。

请注意,我不是自己提出这个问题的。这在编译器理论中是众所周知的结果。在《老虎书》中有讨论。(您也许可以看到他们在Google图书中谈论的话题。


1
@ njzk2:我们试图证明不可能构建消除所有失效代码的失效代码消除器,而不是不可能构建消除某些失效代码的失效代码消除器。使用控制流图技术可以很容易地消除“返回后打印”示例,但是并不是所有的无效代码都可以通过这种方式消除。
user2357112支持Monica 2015年

4
该答案引用了注释。阅读答案时,我需要跳到注释中,然后返回答案。这是令人困惑的(当您认为评论脆弱且可能会丢失时,这很容易使人困惑)。一个完整的答案将更容易阅读。
TRiG

1
@TomášZato-考虑将变量递增并检查是否为奇数的完美数字的程序,该程序仅在找到这样的数字时才终止。显然,该程序不依赖于任何外部输入。您是否在断言可以轻松确定此程序是否终止?ñnn
Gregory J. Puleo 2015年

3
@TomášZato您对暂停问题的理解是错误的。给定有限的图灵机和有限的输入,就不可能确定在运行时是否无限循环。我还没有严格地证明这一点,因为它已经被一遍又一遍地证明,并且是计算机科学的基本原理。维基百科上有一个很好的证明草图x M xMxMx
jmite

1
jmite,请在答案中加入有效的注释,以便答案独立存在。然后标记所有过时的注释,以便我们进行清理。谢谢!
拉斐尔

14

这是对jmite答案的一种扭曲,它避免了有关非终止的潜在混淆。我将提供一个始终停止运行,可能包含无效代码的程序,但我们无法(始终)通过算法确定是否具有该程序。

考虑以下类别的死代码标识符输入:

simulateMx(n) {
  simulate TM M on input x for n steps
  if M did halt
    return 0
  else
    return 1
}

由于Mx是固定的,simulateMs因此return 0当且仅当M不停止时才具有无效代码x

这立即使我们从暂停问题减少到死代码检查:给定TM作为暂停问题实例,使用代码创建上述程序-当且仅当不自行停止时,它才具有死代码码。M MMxMM

因此,死代码检查不可计算。

在这种情况下,如果您不熟悉还原作为证明技术,建议您参考我们的参考资料


5

在不陷入细节的情况下演示这种属性的一种简单方法是使用以下引理:

引理:对于使用图灵完备语言的任何编译器C,都存在一个undecidable_but_true()不带任何参数并返回布尔值true 的函数,因此C无法预测undecidable_but_true()返回的是true还是false。

请注意,该函数取决于编译器。给定一个函数undecidable_but_true1(),始终可以通过知道该函数返回true或false的知识来增强编译器;但是总会有其他功能undecidable_but_true2()无法涵盖。

证明:根据赖斯定理,“此函数返回真”属性是不确定的。因此,任何静态分析算法都无法为所有可能的功能确定此属性。

推论:给定编译器C,以下程序包含无法检测到的无效代码:

if (!undecidable_but_true()) {
    do_stuff();
}

关于Java的注释:Java语言要求编译器拒绝某些包含无法访问代码的程序,同时明智地要求在所有可访问点提供该代码(例如,非void函数中的控制流必须以return语句结尾)。该语言准确地指定了无法执行的代码分析的执行方式;如果没有,那么就不可能编写可移植程序。给定形式的程序

some_method () {
    <code whose continuation is unreachable>
    // is throw InternalError() needed here?
}

有必要指定在什么情况下无法到达的代码必须后面跟一些其他代码,在什么情况下必须不要在任何后面代码。Java 101中出现了一个Java程序示例,该示例包含无法访问的代码,但不允许Java编译器注意的方式:

String day_of_week(int n) {
    switch (n % 7) {
    case 0: return "Sunday";
    case 1: case -6: return "Monday";
    …
    case 6: case -1: return "Saturday";
    }
    // return or throw is required here, even though this point is unreachable
}

请注意,某些语言的某些编译器可能能够检测到结尾day_of_week是不可达的。
user253751

@immibis是的,例如,根据我的经验,CS101的学生可以做到这一点(尽管诚然,CS101的学生不是声音静态分析仪,但他们通常会忘记负面的情况)。这就是我要说的一部分:这是一个程序示例,其中包含无法访问的代码,Java编译器将不会检测到该代码(至少可以警告但不能拒绝)。
吉尔(Gilles)“所以,别再邪恶了”

1
恐怕引理的措词充其量是一种错误,充其量是最多会引起误解的。仅当您用术语(无限)实例集来表述不确定性时,它才有意义。(编译器确实会为每个函数产生一个答案,我们知道它不一定总是正确的,但是说有一个不确定的实例已关闭。)您在引理和证明之间的段落(与引理不完全匹配)如前所述)试图解决这个问题,但我认为最好制定一个明确正确的引理。
拉斐尔

@Raphael嗯?不,编译器无需回答“此函数是否恒定?”的问题。生成工作代码并不需要将“我不知道”与“否”区分开来,但这在这里无关紧要,因为我们只对编译器的静态分析部分感兴趣,而对代码翻译部分不感兴趣。我不明白您对引理的陈述有何误解或不正确的地方-除非您的意思是我应该写“静态分析器”而不是“编译器”?
吉尔斯(Gilles)'所以

该语句听起来像“不确定性意味着存在无法解决的实例”,这是错误的。(我知道你不是想这样说,但是那是它可以读到那些粗心的/新手的方法,恕我直言。)
拉斐尔

3

jmite的答案适用于该程序是否会退出计算-只是因为它是无限的,所以我不会在它死后调用该代码。

但是,还有另一种方法:一个有答案但未知的问题:

public void Demo()
{
  if (Chess.Evaluate(new Chessboard(), int.MaxValue) != 0)
    MessageBox.Show("Chess is unfair!");
  else
    MessageBox.Show("Chess is fair!");
}

public class chess
{
  public Int64 Evaluate(Chessboard Board, int SearchDepth)
  {
  ...
  }
}

毫无疑问该例程确实包含无效代码-该函数将返回执行一条路径但不执行另一条路径的答案。祝你好运!我的记忆是,理论上没有计算机能够在宇宙的生命周期内解决这个问题。

更详细地:

Evaluate()函数计算如果双方都玩得很好(最大搜索深度),则哪一方在下棋比赛中获胜。

象棋评估员通常会先预测每个可能的移动深度,然后尝试在该点上得分(有时将某些分支扩展得更远,因为半看交换或类似内容会产生非常偏斜的感觉。)由于实际的最大深度是17695的半步搜索是详尽无遗的,它将遍历所有可能的国际象棋游戏。由于所有游戏都结束了,因此无需试图确定每个棋盘的位置好坏(因此也没有理由去看棋盘评估逻辑-永远不会被称为),结果要么是赢,要么是输,要么是平局。如果结果是平局,则游戏是公平的;如果结果不是平局,则为不公平的游戏。为了扩展它,我们得到:

public Int64 Evaluate(Chessboard Board, int SearchDepth)
{
  foreach (ChessMove Move in Board.GetPossibleMoves())
    {
      Chessboard NewBoard = Board.MakeMove(Move);
      if (NewBoard.Checkmate()) return int.MaxValue;
      if (NewBoard.Draw()) return 0;
      if (SearchDepth == 0) return NewBoard.Score();
      return -Evaluate(NewBoard, SearchDepth - 1);
    }
}

还要注意,编译器几乎不可能意识到Chessboard.Score()是无效代码。对国际象棋规则的了解使我们能够弄清楚这一点,但是要弄清楚这一点,您必须知道MakeMove永远不会增加计件数,并且如果计件数保持静态太长时间,Chessboard.Draw()将返回true。 。

请注意,搜索深度为半步,而不是整个步。这种AI例程是O(x ^ n)例程,因此对这种AI例程来说是正常的-再添加一个搜索层会对运行多长时间产生重大影响。


8
您假定检查算法将必须执行计算。一个常见的谬论!不,您不会对检查程序的工作方式承担任何责任,否则您将无法反驳其存在。
拉斐尔

6
问题请求的证明,这是不可能的检测死代码。您的帖子包含在您的情况的一个例子怀疑这将是难以检测死代码。那不是眼前问题的答案。
David Richerby,2015年

2
@LorenPechtel我不知道,但这不是证明。也参见这里 ; 更清楚地说明您的误解。
拉斐尔

3
如果有帮助,请考虑理论上没有什么可以阻止某人运行其编译器的时间超过整个Universe的寿命。唯一的限制是实用性。即使在复杂性类NONELEMENTARY中,一个可判定的问题也是一个可判定的问题。
别名2015年

4
换句话说,这个答案充其量只是一种试探法,旨在说明为什么构建检测所有无效代码的编译器可能不容易-但这不是不可能的证明。这种示例可能有助于为学生建立直觉,但不是证明。通过提供自己的证据来证明它是有害的。应该对答案进行编辑,以说明这是建立直觉的例子,而不是不可能的证明。
DW

-3

我认为在计算过程中,死代码的概念在理解编译时间和运行时间之间的差异方面很有趣!

编译器可以确定您何时获得了在任何编译时都无法遍历的代码,但对于运行时却无法这样做。一个带有用户输入的简单的while循环,用于中断测试。

如果编译器实际上可以确定运行时无效代码(即辨别Turing是否完成),那么就有一个论点认为该代码永远不需要运行,因为该工作已经完成!

如果没什么,通过编译时无效代码检查的代码的存在说明了对输入和常规代码卫生(在实际项目的真实世界中)进行务实的边界检查的必要性。


1
该问题要求证明不可能检测到无效代码。您尚未回答该问题。
David Richerby,2015年

另外,您的断言“编译器可以确定何时有遍历任何编译时方案的代码”是错误的,并且直接与问题要求您证明的内容相矛盾。
David Richerby,2015年

@David Richerby,我认为您可能误读了我。我不是在建议编译时检查可以找到所有无效代码,当然不是。我建议在编译时可辨别的所有无效代码集中有一个子集。如果我写:if(true == false){print(“ something”);},则该print语句在编译时将被识别为无效代码。您是否不同意这是您主张的反例?
dwoz 2015年

当然,您可以确定一些无效代码。但是,如果您没有条件就说“确定[您有[无效代码]的时间]”,那么对我而言,这意味着找到所有无效代码,而不仅仅是其中的一部分。
David Richerby
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.