使用一种语言时,我发现准确了解我的代码是如何编译的很有用。例如,我喜欢使用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 % 2
和n / 2
的优化与{ {1}}和quot n 2
。如果我能看到反汇编的代码,就可以自己解决。
我发现的一些资源包括:
这些资源确实有用,但是要完全理解它们,我觉得我需要实际使用GHC编译后的代码。
答案 0 :(得分:1)
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起已过时。quot
in preference to div
来获得最大效率,那么经常重复使用的建议就不适用了,因为`div` 2
生成的代码效率更高。但是,要使用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会话中诊断这些问题非常困难。
-ddump-xxx
标志来转储这些标志。通常,有很多不必要的细节,因此添加标志-dsuppress-all
有时还添加标志-dsuppress-uniques
有助于降低噪音。另外,-fforce-recomp
在多次运行ghc
以查看不同形式时很有用,因为即使源未更改,它也会强制重新编译(并转储所需的输出)。因此,您可以使用以下命令转储反欺诈通道的输出:
ghc -O2 -fforce-recomp -ddump-ds -dsuppress-all -dsuppress-uniques Quot.hs
无论如何,这是最受关注的中间形式:
-ddump-ds
),这是Haskell语言的简化版本,至今仍可识别为Haskell。此时尚未执行任何优化,因此,此转储对于仅了解Core语法及其与“实际” Haskell的区别是最有用的。 Core
的一些最佳文档位于GHC源代码树中-请参见ghc/compiler/coreSyn/CoreSyn.hs
和Expr
类型的定义。-ddump-simpl
)产生的Core输出,您可以学到很多有关程序可能的性能的信息。-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很有帮助,尽管我认为其中的示例已经过时了。 -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
函数已被删除,并最终输出到编译器中。