【问题标题】:Understanding how GHC-compiled code works at the lowest level了解 GHC 编译的代码如何在最低级别工作
【发布时间】:2020-04-06 03:00:42
【问题描述】:

在使用一种语言时,我发现准确了解我的代码是如何编译的很有用。例如,我喜欢使用 C,因为我可以在 GDB 等调试器中检查反汇编代码,并快速查看我编写的 C 代码的实际行为。

Haskell Wiki 有一些 debugging advice,但我还没有找到查看 GHC 生成的反汇编代码的方法。用 GDB 看它不是很有帮助,因为我不知道如何找到特定函数的代码、找出特定变量的存储位置、设置断点或我可以用 C 程序做的任何其他事情.

我的问题:我怎样才能以一种有用的方式反汇编 GHC 生成的代码? 有没有工具可以生成 GHC 生成的代码的注释列表?有没有办法以一种你可以看到编译代码如何“滴答”的方式来编译函数?

一些额外的背景和参考资料:

我想这样做的一个原因是我认为这种理解对于获得良好的性能非常重要(例如,C vs Haskell Collatz conjecture speed comparison)。 performance 上的 Haskell wiki 条目没有详细说明该 SO 问题的答案(即 C 编译器对 n % 2n / 2 的优化与对 quot n 2rem n 2 的优化不足在 GHC 中)。如果我能看到反汇编的代码,我可以自己弄清楚。

我发现的一些资源包括:

这些资源确实很有用,但要完全理解它们,我觉得我需要查看实际编译的 GHC 代码。

【问题讨论】:

  • 为了迂腐,您是在询问 Haskell 实现(大概是特别是 GHC)。 Haskell 本身的工作原理在 Haskell Report 中定义。
  • 反汇编器帮不了你,因为现代机器的汇编语言不能很好地描述 Haskell 语义。据我所知(我可能不准确),GHC翻译成G机的抽象代码,然后用汇编描述这段代码的解释。
  • 当然不需要反汇编。您可以要求 GHC 输出它生成的程序集或 LLVM 代码。

标签: haskell


【解决方案1】:

在 Gnu 调试器下运行 GHC 程序

GHC 编译器为debugging programs under GDB 提供了一些支持。如果你有一个程序:

-- Quot.hs

{-# NOINLINE quot2 #-}
quot2 :: Int -> Int
quot2 n = n `quot` 2

main = print $ quot2 10

您可以使用-g 标志编译它,并使用上面链接中的说明在 GDB 下运行它:

$ ghc -O2 -g -rtsopts Quot.hs    # Note: I'm using GHC 8.6.5
$ gdb --args ./Quot +RTS -V0

可以在quot2 n = n `quot` 2这一行设置断点,反汇编,单步执行相关代码:

(gdb) break Quot.hs:5
Breakpoint 1 at 0x4062c0: file Quot.hs, line 5.
(gdb) run
Starting program: /u/buhr/src/haskell/Quot +RTS -V0
...
Breakpoint 1, Main_zdwquot2_info () at Quot.hs:5
(gdb) display/10i $rip
1: x/10i $rip
=> 0x4062c0 <Main_zdwquot2_info>:   mov    %r14,%rax
   0x4062c3 <Main_zdwquot2_info+3>: shr    $0x3f,%rax
   0x4062c7 <Main_zdwquot2_info+7>: mov    %r14,%rbx
   0x4062ca <Main_zdwquot2_info+10>:    add    %rax,%rbx
   0x4062cd <Main_zdwquot2_info+13>:    sar    %rbx
   0x4062d0 <Main_zdwquot2_info+16>:    jmpq   *0x0(%rbp)
   ...
(gdb) p $r14
$1 = 10
(gdb)

(这里的Main_zdwquot2_info 符号是一个错位的名称。有关这方面的一些信息,请参见下文。)无论如何,从这个反汇编中您可以看到优化的quot2 代码使用几个右移来执行等效的:

((n < 0 : 1 ? 0) + n) >> 1

将负数截断到零。

如果改用div,代码将如下所示:

1: x/10i $rip
=> 0x4062c0 <Main_zdwdiv2_info>:    mov    %r14,%rbx
   0x4062c3 <Main_zdwdiv2_info+3>:  sar    %rbx
   0x4062c6 <Main_zdwdiv2_info+6>:  jmpq   *0x0(%rbp)
   ...

所以,即使我会说你无法从反汇编 GHC 代码中学到任何东西,我想我错了。我们已经了解到:

  • answer 不正确,或者至少在 GHC 8.6.5 中已过时,因为 `quot` 2`div` 2 已针对班次进行了优化。
  • 经常重复使用quot in preference to div 以获得最大效率的建议在除以2 时不适用,因为`div` 2 生成的代码效率更高。

转储 GHC 中间表格

然而,使用 GDB 跟踪 Haskell 代码执行仍然真的困难。如果您在主入口点(通常称为Main_main_info)设置了断点:

(gdb) break Main_main_info
Breakpoint 2 at 0x4063c8: file Div.hs, line 7.
(gdb) run
...
Breakpoint 2, Main_main_info () at Div.hs:7
1: x/10i $rip
=> 0x4063c8 <Main_main_info>:   mov    $0x4ac622,%edi
   0x4063cd <Main_main_info+5>: mov    $0x4a4348,%esi
   0x4063d2 <Main_main_info+10>:    mov    $0x4a73b8,%r14d
   0x4063d8 <Main_main_info+16>:    jmpq   0x433de8 <base_GHCziIOziHandleziText_hPutStrzq_info>
...

很容易迷路。这里,$0x4ac622 是 Haskell 常量 True 的闭包,$0x4a4348 是 quot2 10 结果的可打印字符串表示形式的闭包,$0x4a73b8 是 stdout 的闭包,所有这些都设置为GHC.IO.Handle.Text.hPutStr' 来电。

在 GHC 代码中,函数是用jmpq 调用的,而不是callq,所以你不能真正通过(gdb) next 调用你对跟踪不感兴趣的调用。此外,由于惰性评估模型,在调用函数之前不会评估参数,因此要从头开始跟踪 quot2 10 调用的执行,您需要跟踪 hPutStr' 的执行以找到它强制对结果的可打印表示的闭包进行评估,然后跟踪对该闭包的评估以查看它在何处强制对 quot2 10 本身进行评估。

另一个问题是与原始 Haskell 代码相比,该程序集确实低级。尽管您可能能够学到一些关于“微优化”的知识,例如 `div` 2 是如何实现的,但大多数 Haskell 性能问题是由更高级别的低效率引起的(例如,不必要的值装箱、列表融合失败等) .从 GDB 会话中诊断这些问题非常困难。

因此,大多数寻求理解 GHC 编译代码的人都会查看各种形式的中间编译器输出。正如您可能从研究中发现的那样,GHC 编译器通过中间形式执行许多转换。这些可以使用一系列-ddump-xxx 标志转储。通常,有很多不必要的细节,因此添加标志 -dsuppress-all 有时还添加标志 -dsuppress-uniques 有助于降低噪音。此外,-fforce-recomp 在多次运行ghc 以查看不同的形式时很有帮助,因为即使源没有更改,它也会强制重新编译(并转储所需的输出)。因此,您可以使用以下方式转储脱糖过程的输出:

ghc -O2 -fforce-recomp -ddump-ds -dsuppress-all -dsuppress-uniques Quot.hs

无论如何,这里是最感兴趣的中间形式:

  • 原始源代码已“脱糖”到 Core (-ddump-ds),它是 Haskell 语言的简化版本,仍然可以识别为 Haskell。此时尚未执行任何优化,因此此转储对于了解 Core 语法以及它与“真正的”Haskell 有何不同非常有用。 Core 的一些最佳文档位于 GHC 源代码树中 - 请参阅 ghc/compiler/coreSyn/CoreSyn.hsExpr 类型的定义。
  • 大多数高级优化都是通过应用核心到核心的转换函数在核心上执行的。通过研究简化器 (-ddump-simpl) 产生的核心输出,您可以了解很多关于程序可能性能的信息。
  • 简化后的 Core 转换为 STG (-ddump-stg)。与 Core 不同,STG 显然是“不是 Haskell”。它是一种无类型的函数式语言,最能代表 GHC 程序“实际运行”的方式。将您面前的 STG 作为路线图/参考,您可能可以处理较低级别的表示,包括使用调试器逐步执行反汇编代码,并了解它实际在做什么。我发现了解 STG 的最佳参考是论文 How to make a fast curry。在论文中,STG 基本上是图 1 中描述的语言,生成的汇编代码对应于图 2 中的“评估规则”。您可能还会发现I know kung fu: learning STG by example 有帮助,尽管我认为那里的示例已经过时了。李>
  • 在 STG 之下,较低级别的形式是 CMM (-ddump-cmm) 和程序集 (-ddump-asm)。 CMM 是一种类似 C 的汇编语言。我没有找到任何特别好的/准确的文档,尽管它通常比汇编更容易理解,而汇编通常比 GDB 下的反汇编更容易理解,因为它包含调试信息中缺少的其他符号.通过将 GHC 转储的程序集与我在 GDB 下看到的程序集进行比较,我能够识别上述示例中 True 等的闭包。

我发现我通常必须并排查看 STG、CMM 和程序集,才能弄清楚某些 GHC 代码是如何工作的。

所以,也许这会让你开始。

名称修改

如上所述,您将在 GDB 中看到的可执行文件中的名称已从转储编译器输出(包括 -ddump-asm)中的表单中删除。我不知道这是否记录在任何地方,但这是我从 GHC 源代码 (ghc/compiler/utils/Encoding.hs) 获得的表格:

'(' ="ZL"   'Z' ="ZZ"  '$' ="zd"  '<' ="zl"  '\\'="zr"
')' ="ZR"   'z' ="zz"  '=' ="ze"  '-' ="zm"  '/' ="zs"
'[' ="ZM"   '&' ="za"  '>' ="zg"  '!' ="zn"  '*' ="zt"
']' ="ZN"   '|' ="zb"  '#' ="zh"  '+' ="zp"  '_' ="zu"
':' ="ZC"   '^' ="zc"  '.' ="zi"  '\''="zq"  '%' ="zv"

所以 GDB 中的名称 Main_zdwquot2_info 对应于转储 GHC 输出中的 Main_$wquot2_info。那看起来仍然很混乱,但那是因为它是一个“系统名称”。正如 ghc/compiler/basicTypes/OccName 中的 cmets 中所述:

Making System Names
-------------------

Here's our convention for splitting up the interface file name space:

   d...         dictionary identifiers
                (local variables, so no name-clash worries)

All of these other OccNames contain a mixture of alphabetic
and symbolic characters, and hence cannot possibly clash with
a user-written type or function name

   $f...        Dict-fun identifiers (from inst decls)
   $dmop        Default method for 'op'
   $pnC         n'th superclass selector for class C
   $wf          Worker for function 'f'
   $sf..        Specialised version of f
   D:C          Data constructor for dictionary for class C
   NTCo:T       Coercion connecting newtype T with its representation type
   TFCo:R       Coercion connecting a data family to its representation type R

所以上面的函数是quot2 函数的“工人”。通常,对装箱整数进行操作的函数会有一个对未装箱整数进行操作的“worker”对应项。在我们的 Quot.hs 示例中,只需要这个未装箱的工作函数,因此在最终的编译器输出中删除了原始的 quot2 函数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-17
    • 2019-04-24
    • 2017-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多