了解GHC编译的代码如何在最低级别工作

时间:2019-12-12 16:52:14

标签: haskell

使用一种语言时,我发现准确了解我的代码是如何编译的很有用。例如,我喜欢使用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的优化与{ {1}}和quot n 2。如果我能看到反汇编的代码,就可以自己解决。

我发现的一些资源包括:

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

1 个答案:

答案 0 :(得分: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代码中学到任何东西,但我想我错了。我们已经了解到:

  • 由于`quot` 2`div` 2 已针对移位进行了优化,因此该answer是不正确的,或者至少从GHC 8.6.5起已过时。
  • >
  • 如果用2除以quot in preference to div来获得最大效率,那么经常重复使用的建议就不适用了,因为`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类型的定义。
  • 大多数高级优化是通过应用Core-to-Core转换功能在Core上执行的。通过研究简化程序(-ddump-simpl)产生的Core输出,您可以学到很多有关程序可能的性能的信息。
  • 简化的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中的注释所述:

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功能的“工人”。通常,对装箱整数操作的函数将具有对未装箱整数操作的“工人”对应物。在我们的Quot.hs示例中,只需要此未装箱的辅助函数,因此原始的quot2函数已被删除,并最终输出到编译器中。