【问题标题】:Debugging infinite loops in Haskell programs with GHCi使用 GHCi 调试 Haskell 程序中的无限循环
【发布时间】:2011-03-17 09:52:46
【问题描述】:

我第一次在我正在编写的 Haskell 程序中遇到无限循环。我已将其缩小到一个非常具体的代码部分,但我似乎无法准确指出我在哪里有一个非终止递归定义。我对 GHCi 中的 :trace 和 :history 有点熟悉,但问题是我的代码的某些分支涉及到 Data.Map.Map 的大量递归修改,因为地图 x 是由 @987654323 获得的@ing 在地图x' 中的某些东西基于另一个地图中的值,这取决于x'。细节在这里并不重要,但正如您可能知道的那样,如果这种情况以交织的递归方式发生,我的通话记录将完全陷入地图lookups、adjustments 和 @ 中涉及的所有各种比较中987654328@ions.

谁能推荐一种更有效的方法来定位无限循环?例如,将调用历史限制为来自单个源文件的调用会很有帮助。

【问题讨论】:

  • 已经破解了好几行 Haskell,我不得不说我在调试方面从来没有太多运气。始终彻底修改代码和重构最终帮助我找到了问题所在。但当然,这只是轶事。
  • 如果您发布一些代码,社区可能会帮助您调试它。
  • @FUZxxl:是的,这确实是一个很好的调试策略。它帮助了我很多次。我已经能够通过在纸上评估我的表达式来解决我的特定问题,直到我可以看到无限递归的定义。但是,我仍然想了解更多关于不同调试技术的信息,所以我将留下这个问题。

标签: debugging haskell infinite-loop ghc ghci


【解决方案1】:

我很惊讶没有人提到所有 Haskell 性能问题都会得到的普遍响应(无限运行时是“性能问题”的一个相当极端的例子):分析!

我只是能够使用分析快速识别无限循环。为了完整起见,使用-prof -fprof-auto 进行编译,然后运行程序足够长的时间,使有问题的函数应该在分析统计中明显。例如,我希望我的程序在 1 秒内完成,所以我让分析器运行大约 30 秒,然后用 Ctrl+C 终止我的程序。 (注意:Profiling 会保存增量结果,因此即使您在程序运行完成之前将其终止,您仍然可以获得有意义的数据。编辑Except when it doesn't.

在 .prof 文件中,我找到了以下块:

                                                 individual      inherited
COST CENTRE         MODULE     no.    entries   %time  %alloc   %time %alloc
...
primroot.\          Zq         764          3    10.3    13.8    99.5  100.0
 primroot.isGen     Zq         1080   50116042    5.3     6.9    89.2   86.2
  primroot.isGen.\  Zq         1087   50116042   43.4    51.7    83.8   79.3
   fromInteger      ZqBasic    1088          0   40.4    27.6    40.4   27.6

所以primroot.isGen 有 5000 万个条目,而下一个被调用次数最多的函数只有 1024 个调用。此外,99.5% 的运行时间都用于计算 primroot,这似乎非常可疑。我检查了那个函数并很快发现了错误,在我的例子中是一个简单的错字:(`div` foo) 而不是(div foo)

我认为值得注意的是,GHC 警告不会发现这个问题,-fbreak-on-exceptions 也不会。代码库很大;试图通过插入调试语句(任何类型的)来追踪问题并没有让我到任何地方。我也没有成功使用 GHCi 调试器,因为历史记录基本上不存在,而且 HPC 没有显示任何有用的信息。

【讨论】:

  • 使用stack ghci时,可以使用--profile启用分析。
  • Neil Mitchell 在此处提出了一种制作损坏的 .hp 文件的方法:neilmitchell.blogspot.com/2013/02/…,即“删除最后一个 END_SAMPLE 之后的所有内容以恢复 hp2ps 可以理解的配置文件。”
  • 这似乎不适用于所有情况。我只是得到一个空(0 字节)的空白分析文件。
  • 也许你遇到了上面提到的“除非它没有”的问题?
【解决方案2】:

正如 ShiDoiSi 所说,“肉眼”解决往往是最成功的方法。

如果您在相同的函数中绑定到不同的类似命名的变量 x、x' 等,您可以尝试在文件顶部启用警告:

{-# OPTIONS -Wall #-} 

如果您绑定到错误的东西并进行失控递归是一个问题,这可能会帮助您发现它 - 例如通过表明对 shadowing 的意外使用。

【讨论】:

  • 非常感谢!我开始发疯,终于能够以这种方式找到问题。这是一个阴影,但更难发现,因为它以匹配的模式出现......
【解决方案3】:

确保您已完全使用 GHCi 调试器,包括设置 -fbreak-on-exception(如果您收到 <<loop>> 很有用,是吗?)并确保您已尝试过 Stephen 的建议使用 GHC 的警告。

如果这些失败(GHCi 调试器真的不应该“失败”,这只是解释数据的问题)然后尝试在循环案例上运行HPC,这样你就可以直观地看到分支和不存在的值评估,如果它正在循环,那么应该完成的事情可能甚至没有被评估,并且将显示在标记的 HTML 中。

【讨论】:

  • 所有下载 HPC 的链接似乎都已失效。
  • @Eric: cabal install hpc
  • Here 的实际使用 HPC。
  • 或许您可以解释一下如何使用 HPC 来检测无限循环?我生成了一个标记报告,但其中没有任何内容可以将我指向我的特定无限循环。
  • @Eric 这比评论更适合新问题。简而言之:ghc -fhpc x.hs ; ./x ; hpc markup x ; xdg-open Main.hs.html。 HTML 应该包含任何未评估为黄色的内容。如果你有相对线性相关的一系列 let 语句(这个问题经常出现),你会看到类似二进制操作的东西,它需要两个参数,但一个参数是黄色的,而另一个不是 - 另一个参数可能是循环的。这不是调试的好方法,调试器是首选,但这是我听说人们使用的一种方法。
【解决方案4】:

我正在进行长时间的调试,以查找无限循环的原因。我已经非常接近了,这是对我帮助最大的。假设您的循环是由以下原因引起的:

...
x1 = f1 x2 y
x2 = f2 z x3
x3 = f3 y x1
...

所以 x1 取决于 x2,x2 取决于 x3,x3 取决于 x1。糟糕!

在 f1, f2, f3 的定义中添加跟踪函数。比如:

f1 x y | trace ("f1: ") False = undefined
f1 x y = ... -- definition of f1

f2 x y | trace ("f2: ") False = undefined
f2 x y = ... -- definition of f2

-- same for f3

运行您的程序以查看调用了这些函数中的哪一个。输出可能类似于

f3:
f2:
f1: 
<<loop>>

然后开始在跟踪函数中显示一些变量。例如,如果您将 f2 的跟踪更改为

f2 x y | trace ("f2: x: " ++ show x) False = undefined

那么输出将类似于:

f3:
f2: x: x_value
f1: 
<<loop>>

但是如果你再把 f2 的轨迹改成

f2 x y | trace ("f2: x: " show x ++ " y: " ++ show y) False = undefined

那么输出会是

f3:
<<loop>>

因为 f2 的第二个参数由于循环依赖而无法计算。

所以你现在知道无限循环中的一个函数是 f2 并且它的第二个参数(但不是它的第一个参数)具有循环依赖关系。

调试愉快!

【讨论】:

    【解决方案5】:

    您不能使用 :back 和 :forward 来访问您的历史/跟踪并找出您的地图在调用之间的演变吗?

    您应该能够发现导致递归循环的模式。

    --如果它太棘手,你可能已经编写了一些太聪明而无法调试的代码(或者可能太复杂,你应该重构它^^)--

    【讨论】:

    • 如果:history 只提供几个断点,或者根本没有断点,这将不起作用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-10-05
    • 2016-01-27
    • 1970-01-01
    • 2017-05-14
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    相关资源
    最近更新 更多