GHC可以预期哪些优化可靠地执行?

GHC有很多可以执行的优化,但是我不知道它们是什么,也不知道它们在什么情况下被执行。

我的问题是:我可以期待什么样的转变,每一次或几乎这样呢? 如果我看一段经常被执行(评估)的代码,我的第一个想法是“嗯,也许我应该优化它”,在这种情况下,我应该第二个想法是“甚至不要去想它, GHC得到这个“?

我正在读“ Stream Fusion:从List到Streams到Nothing”一文 ,以及他们用于将列表处理重写为不同forms的技术,GHC的正常优化将可靠地优化为简单循环,这对我来说是新颖的。 如何知道我的程序何时可以进行这种优化?

GHC手册中提供了一些信息 ,但它只是回答这个问题的一部分。

编辑:我开始赏金。 我想要的是像lambda / let / case-floating,types/构造函数/函数参数专门化,严格分析和拆箱,worker / wrapper,以及其他重要的GHC所做的我已经省略的低级转换列表,以及input和输出代码的解释和实例,并且当总效应大于其部分总和时,理想地说明情况。 理想的情况下,提到什么时候转型不会发生。 我并不期望对每一个转换都有新颖的解释,只要大局就是这样,几句话和一行代码就足够了(或者是一个链接,如果不是二十页的科学论文)清除它的结尾。 我希望能够看到一段代码,并能够很好地猜测它是否会被编译成一个紧密的循环,或者为什么不是,或者我将不得不改变来做出来。 (我对stream融合这样的大型优化框架(我只是读了一篇关于这方面的文章)并不是很感兴趣;更多的是这些框架的人所拥有的知识。

这个GHC Trac页面也很好地解释了通行证。 这个页面解释了优化顺序,但是,就像Trac的大部分Wiki一样,它已经过时了。

具体来说,最好的办法可能是看一个特定的程序是如何编译的。 查看正在执行哪些优化的最佳方法是使用-v标志详细编译程序。 以我能在电脑上find的第一个Haskell为例:

 Glasgow Haskell Compiler, Version 7.4.2, stage 2 booted by GHC version 7.4.1 Using binary package database: /usr/lib/ghc-7.4.2/package.conf.d/package.cache wired-in package ghc-prim mapped to ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7 wired-in package integer-gmp mapped to integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43 wired-in package base mapped to base-4.5.1.0-6e4c9bdc36eeb9121f27ccbbcb62e3f3 wired-in package rts mapped to builtin_rts wired-in package template-haskell mapped to template-haskell-2.7.0.0-2bd128e15c2d50997ec26a1eaf8b23bf wired-in package dph-seq not found. wired-in package dph-par not found. Hsc static flags: -static *** Chasing dependencies: Chasing modules from: *SleepSort.hs Stable obj: [Main] Stable BCO: [] Ready for upsweep [NONREC ModSummary { ms_hs_date = Tue Oct 18 22:22:11 CDT 2011 ms_mod = main:Main, ms_textual_imps = [import (implicit) Prelude, import Control.Monad, import Control.Concurrent, import System.Environment] ms_srcimps = [] }] *** Deleting temp files: Deleting: compile: input file SleepSort.hs Created temporary directory: /tmp/ghc4784_0 *** Checking old interface for main:Main: [1 of 1] Compiling Main ( SleepSort.hs, SleepSort.o ) *** Parser: *** Renamer/typechecker: *** Desugar: Result size of Desugar (after optimization) = 79 *** Simplifier: Result size of Simplifier iteration=1 = 87 Result size of Simplifier iteration=2 = 93 Result size of Simplifier iteration=3 = 83 Result size of Simplifier = 83 *** Specialise: Result size of Specialise = 83 *** Float out(FOS {Lam = Just 0, Consts = True, PAPs = False}): Result size of Float out(FOS {Lam = Just 0, Consts = True, PAPs = False}) = 95 *** Float inwards: Result size of Float inwards = 95 *** Simplifier: Result size of Simplifier iteration=1 = 253 Result size of Simplifier iteration=2 = 229 Result size of Simplifier = 229 *** Simplifier: Result size of Simplifier iteration=1 = 218 Result size of Simplifier = 218 *** Simplifier: Result size of Simplifier iteration=1 = 283 Result size of Simplifier iteration=2 = 226 Result size of Simplifier iteration=3 = 202 Result size of Simplifier = 202 *** Demand analysis: Result size of Demand analysis = 202 *** Worker Wrapper binds: Result size of Worker Wrapper binds = 202 *** Simplifier: Result size of Simplifier = 202 *** Float out(FOS {Lam = Just 0, Consts = True, PAPs = True}): Result size of Float out(FOS {Lam = Just 0, Consts = True, PAPs = True}) = 210 *** Common sub-expression: Result size of Common sub-expression = 210 *** Float inwards: Result size of Float inwards = 210 *** Liberate case: Result size of Liberate case = 210 *** Simplifier: Result size of Simplifier iteration=1 = 206 Result size of Simplifier = 206 *** SpecConstr: Result size of SpecConstr = 206 *** Simplifier: Result size of Simplifier = 206 *** Tidy Core: Result size of Tidy Core = 206 writeBinIface: 4 Names writeBinIface: 28 dict entries *** CorePrep: Result size of CorePrep = 224 *** Stg2Stg: *** CodeGen: *** CodeOutput: *** Assembler: '/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-I.' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' 'SleepSort.o' Upsweep completely successful. *** Deleting temp files: Deleting: /tmp/ghc4784_0/ghc4784_0.c /tmp/ghc4784_0/ghc4784_0.s Warning: deleting non-existent /tmp/ghc4784_0/ghc4784_0.c link: linkables are ... LinkableM (Sat Sep 29 20:21:02 CDT 2012) main:Main [DotO SleepSort.o] Linking SleepSort ... *** C Compiler: '/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.c' '-o' '/tmp/ghc4784_0/ghc4784_0.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include' *** C Compiler: '/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' '/tmp/ghc4784_0/ghc4784_1.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include' *** Linker: '/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-o' 'SleepSort' 'SleepSort.o' '-L/usr/lib/ghc-7.4.2/base-4.5.1.0' '-L/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0' '-L/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0' '-L/usr/lib/ghc-7.4.2' '/tmp/ghc4784_0/ghc4784_0.o' '/tmp/ghc4784_0/ghc4784_1.o' '-lHSbase-4.5.1.0' '-lHSinteger-gmp-0.4.0.0' '-lgmp' '-lHSghc-prim-0.2.0.0' '-lHSrts' '-lm' '-lrt' '-ldl' '-u' 'ghczmprim_GHCziTypes_Izh_static_info' '-u' 'ghczmprim_GHCziTypes_Czh_static_info' '-u' 'ghczmprim_GHCziTypes_Fzh_static_info' '-u' 'ghczmprim_GHCziTypes_Dzh_static_info' '-u' 'base_GHCziPtr_Ptr_static_info' '-u' 'base_GHCziWord_Wzh_static_info' '-u' 'base_GHCziInt_I8zh_static_info' '-u' 'base_GHCziInt_I16zh_static_info' '-u' 'base_GHCziInt_I32zh_static_info' '-u' 'base_GHCziInt_I64zh_static_info' '-u' 'base_GHCziWord_W8zh_static_info' '-u' 'base_GHCziWord_W16zh_static_info' '-u' 'base_GHCziWord_W32zh_static_info' '-u' 'base_GHCziWord_W64zh_static_info' '-u' 'base_GHCziStable_StablePtr_static_info' '-u' 'ghczmprim_GHCziTypes_Izh_con_info' '-u' 'ghczmprim_GHCziTypes_Czh_con_info' '-u' 'ghczmprim_GHCziTypes_Fzh_con_info' '-u' 'ghczmprim_GHCziTypes_Dzh_con_info' '-u' 'base_GHCziPtr_Ptr_con_info' '-u' 'base_GHCziPtr_FunPtr_con_info' '-u' 'base_GHCziStable_StablePtr_con_info' '-u' 'ghczmprim_GHCziTypes_False_closure' '-u' 'ghczmprim_GHCziTypes_True_closure' '-u' 'base_GHCziPack_unpackCString_closure' '-u' 'base_GHCziIOziException_stackOverflow_closure' '-u' 'base_GHCziIOziException_heapOverflow_closure' '-u' 'base_ControlziExceptionziBase_nonTermination_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnMVar_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnSTM_closure' '-u' 'base_ControlziExceptionziBase_nestedAtomically_closure' '-u' 'base_GHCziWeak_runFinalizzerBatch_closure' '-u' 'base_GHCziTopHandler_flushStdHandles_closure' '-u' 'base_GHCziTopHandler_runIO_closure' '-u' 'base_GHCziTopHandler_runNonIO_closure' '-u' 'base_GHCziConcziIO_ensureIOManagerIsRunning_closure' '-u' 'base_GHCziConcziSync_runSparks_closure' '-u' 'base_GHCziConcziSignal_runHandlers_closure' link: done *** Deleting temp files: Deleting: /tmp/ghc4784_0/ghc4784_1.o /tmp/ghc4784_0/ghc4784_0.s /tmp/ghc4784_0/ghc4784_0.o /tmp/ghc4784_0/ghc4784_0.c *** Deleting temp dirs: Deleting: /tmp/ghc4784_0 

从第一个*** Simplifier:到最后,所有的优化阶段发生,我们看到了很多。

首先,简化器几乎在所有阶段之间运行。 这使得写许多遍更容易。 例如,在实现许多优化时,他们只需创build重写规则来传播更改,而不必手动执行。 简化器包含许多简单的优化,包括内联和融合。 我知道的主要限制是GHC拒绝内联recursion函数,并且必须正确地命名事物以便融合才能工作。

接下来,我们看到所有执行的优化的完整列表:

  • 专攻

    专业化的基本思想是通过标识函数被调用的地方来删除多态和重载,并且创build不是多态的函数版本 – 它们是特定于被调用的types的。 你也可以告诉编译SPECIALISE pragma来做到这一点。 作为一个例子,采取阶乘函数:

     fac :: (Num a, Eq a) => a -> a fac 0 = 1 fac n = n * fac (n - 1) 

    由于编译器不知道要使用的乘法的任何属性,所以根本无法对其进行优化。 但是,如果它看到它在一个Int上使用,它现在可以创build一个新的版本,仅在types上有所不同:

     fac_Int :: Int -> Int fac_Int 0 = 1 fac_Int n = n * fac_Int (n - 1) 

    接下来,下面提到的规则可能会触发,最终你会得到一些在unboxed Int上工作的东西,这比原来快得多。 查看专业化的另一种方法是对types字典和typesvariables的部分应用。

    这里的源文件中有大量的注释。

  • 漂浮

    编辑:我显然以前误解了这一点。 我的解释已经完全改变了。

    这个的基本思想是移动不应该重复出function的计算。 例如,假设我们有这个:

     \x -> let y = expensive in x+y 

    在上面的lambda中,每次函数被调用时, y被重新计算。 漂浮出来的更好的function是

     let y = expensive in \x -> x+y 

    为了促进这个过程,可以应用其他转换。 例如,这发生了:

      \x -> x + f 2 \x -> x + let f_2 = f 2 in f_2 \x -> let f_2 = f 2 in x + f_2 let f_2 = f 2 in \x -> x + f_2 

    同样,重复的计算被保存。

    在这种情况下源是非常可读的。

    目前两个相邻的lambda的绑定不会浮动。 例如,这不会发生:

     \xy -> let t = x+x in ... 

    即将

      \x -> let t = x+x in \y -> ... 
  • 向内浮动

    引用源代码,

    floatInwards的主要目的是浮动到一个案例的分支,所以我们不分配东西,把它们保存在堆栈中,然后发现它们在所选分支中是不需要的。

    举个例子,假设我们有这个expression式:

     let x = big in case v of True -> x + 1 False -> 0 

    如果v评价为False ,那么通过分配x ,这大概是一个大的thunk,我们浪费了时间和空间。 浮动内部修复这个,产生这样的:

     case v of True -> let x = big in x + 1 False -> let x = big in 0 

    ,随后被简化器replace

     case v of True -> big + 1 False -> 0 

    本文虽然涵盖了其他主题,但给出了相当清晰的介绍。 请注意,尽pipe他们的名字,浮动和浮动不会陷入无限循环有两个原因:

    1. 漂浮在浮动状态让case说明,而浮动交易与function。
    2. 有一个固定的通行顺序,所以他们不应该交替无限。
  • 需求分析

    需求分析或严格分析不像一个信息收集过程那样是一个转变,更像名称所暗示的那样。 编译器查找总是评估它们的参数(或者至less其中一些参数)的函数,并且使用按值调用而不是按需调用来传递这些参数。 既然你逃避了thunk的开销,这往往要快得多。 Haskell中的许多性能问题都是由于这个传递失败,或者代码不够严格。 一个简单的例子是使用foldrfoldlfoldl'来总结一个整数列表之间的区别 – 第一个原因导致堆栈溢出,第二个导致堆溢出,并且上次运行正常,因为严格。 这可能是所有这些最容易理解和最好logging的。 我相信多态性和CPS代码经常打败这个。

  • 工人包装绑定

    工人/包装物转换的基本思想是在简单的结构上进行紧密的循环,转换到两端的结构。 例如,用这个函数计算一个数的阶乘。

     factorial :: Int -> Int factorial 0 = 1 factorial n = n * factorial (n - 1) 

    使用GHC中的Int定义,我们有

     factorial :: Int -> Int factorial (I# 0#) = I# 1# factorial (I# n#) = I# (n# *# case factorial (I# (n# -# 1#)) of I# down# -> down#) 

    注意代码是如何覆盖在I#的? 我们可以通过这样删除它们:

     factorial :: Int -> Int factorial (I# n#) = I# (factorial# n#) factorial# :: Int# -> Int# factorial# 0# = 1# factorial# n# = n# *# factorial# (n# -# 1#) 

    虽然这个具体的例子也可以通过SpecConstr来完成,但是工作者/包装器的转换在它可以做的事情上是非常普遍的。

  • 常见的子expression式

    这是另一个非常简单的优化,就像严格性分析一样非常有效。 基本思想是,如果你有两个相同的expression式,它们将具有相同的值。 例如,如果fib是斐波那契数字计算器,则CSE将转换

     fib x + fib x 

     let fib_x = fib x in fib_x + fib_x 

    这将计算减半。 不幸的是,这有时会妨碍其他优化。 另一个问题是这两个expression式必须在同一个地方,它们必须在语法上是相同的,不同的价值观。 例如,CSE不会在没有一堆内联的情况下触发下面的代码:

     x = (1 + (2 + 3)) + ((1 + 2) + 3) y = fx z = g (fx) y 

    但是,如果您通过llvm进行编译,由于其全局值编号传递,您可能会得到一些结合。

  • 解放案件

    这似乎是一个非常有据可查的转变,除了它可能导致代码爆炸的事实。 这里是我find的小文档的一个重新格式化(并稍微改写)的版本:

    该模块遍历Core ,并查找自由variables的情况。 标准是:如果在recursion调用的路由上有一个自由variables的case ,那么recursion调用被replace为展开。 例如,在

     f = \ t -> case v of V ab -> a : ft 

    内部f被replace。 做

     f = \ t -> case v of V ab -> a : (letrec f = \ t -> case v of V ab -> a : ft in f) t 

    注意需要阴影。 简化,我们得到

     f = \ t -> case v of V ab -> a : (letrec f = \ t -> a : ft in ft) 

    这是更好的代码,因为在内部letrec内部是空闲的,而不是需要v投影。 请注意,这涉及到自由variables ,与SpecConstr不同,后者处理已知forms的参数

    有关SpecConstr的更多信息,请参见下文。

  • SpecConstr – 这个转换程序

     f (Left x) y = somthingComplicated1 f (Right x) y = somethingComplicated2 

     f_Left xy = somethingComplicated1 f_Right xy = somethingComplicated2 {-# INLINE f #-} f (Left x) = f_Left x f (Right x) = f_Right x 

    作为一个扩展的例子,用last定义:

     last [] = error "last: empty list" last (x:[]) = x last (x:x2:xs) = last (x2:xs) 

    我们先把它转换成

     last_nil = error "last: empty list" last_cons x [] = x last_cons x (x2:xs) = last (x2:xs) {-# INLINE last #-} last [] = last_nil last (x : xs) = last_cons x xs 

    接下来,简化器运行,我们有

     last_nil = error "last: empty list" last_cons x [] = x last_cons x (x2:xs) = last_cons x2 xs {-# INLINE last #-} last [] = last_nil last (x : xs) = last_cons x xs 

    请注意,该程序现在更快,因为我们不会重复装箱和拆箱清单的前面。 还要注意内联是至关重要的,因为它允许实际使用新的,更有效的定义,并使recursion定义更好。

    SpecConstr受到许多启发式的控制。 文中提到的是这样的:

    1. lambda是明确的,arity是a
    2. 右边是“足够小”,由旗子控制的东西。
    3. 该函数是recursion的,而specializable调用是在右侧使用的。
    4. 该函数的所有参数都存在。
    5. 至less有一个参数是一个构造函数应用程序。
    6. 这个论点在function的某个地方进行了案例分析。

    然而,启发式几乎肯定会改变。 事实上,这篇论文提到了另一种第六种启发式:

    只有在x 由一个case检查的case专门研究一个参数x ,而不是传递给一个普通的函数,或者作为结果的一部分返回。

这是一个非常小的文件(12行),因此可能没有触发这么多的优化(尽pipe我认为它们都做了)。 这也没有告诉你为什么select这些通行证,为什么它把这些通行证顺序。

怠惰

这不是一个“编译器优化”,但它是由语言规范保证的东西,所以你可以永远指望它发生。 从本质上讲,这意味着工作不会执行,直到你“结果”。 (除非你做了几件事情,故意closures懒惰。)

显然,这本身就是一个完整的话题,所以对此已经有了很多的问题和答案。

在我有限的经验中,让你的代码太懒或太严格,会比我要谈论的其他任何东西(在时间空间上)有更大的性能损失。

严格分析

懒惰是要避免工作,除非有必要。 如果编译器可以确定给定的结果将会“总是”需要的话,那么它就不会费劲地存储计算并在稍后执行; 它只会直接执行,因为这样更有效率。 这就是所谓的“严格分析”。

显而易见的是编译器不能总是检测什么时候可以严格执行。 有时你需要给编译器一些提示。 (我不知道有什么简单的方法来确定严格分析是否已经完成了您的想法,除了涉及Core输出。

内联

如果你调用一个函数,并且编译器可以告诉你正在调用哪个函数,那么它可能试图“内联”那个函数 – 也就是用函数本身的一个副本replace函数调用。 函数调用的开销通常很小,但内联经常会使其他优化发生,否则就不会发生,因此内联可能是一个巨大的胜利。

函数只有在“足够小”的情况下才被内联(或者如果添加专用于内联的编译指示)。 另外,如果编译器可以告诉你正在调用的函数,函数只能被内联。 有两种主要的方式,编译器可能无法告诉:

  • 如果你调用的函数是从其他地方传入的。 例如,编译filter函数时,不能内联filter谓词,因为它是用户提供的参数。

  • 如果你调用的函数是一个类方法并且编译器不知道涉及的是什么types。 例如,当sum函数被编译时,编译器不能内联+函数,因为sum和几个不同的数字types一起工作,每个数字types都有不同的+函数。

在后一种情况下,可以使用{-# SPECIALIZE #-}编译指示来生成硬编码为特定types的函数版本。 例如, {-# SPECIALIZE sum :: [Int] -> Int #-}将为Inttypes编译硬编码的sum版本,这意味着+可以在此版本中内联。

但是请注意,我们的新特殊函数只有在编译器能够告诉我们正在处理Int时才会被调用。 否则,原来的多态sum被调用。 再次,实际的函数调用开销是相当小的。 这是额外的优化,内联可以启用哪些是有益的。

常见的子expression式消除

如果某个代码块计算两次相同的值,编译器可能会用相同计算的单个实例replace它。 例如,如果你这样做

 (sum xs + 1) / (sum xs + 2) 

那么编译器可能会优化这个

 let s = sum xs in (s+1)/(s+2) 

你可能会期望编译器总是这样做。 但是,显然在某些情况下,这可能会导致性能下降,而不是更好,所以GHC并不总是这样做。 坦率地说,我不太了解这个背后的细节。 但是底线是,如果这种转变对你很重要,那么手动完成并不困难。 (如果不重要,你为什么要担心呢?)

案例expression式

考虑以下:

 foo (0:_ ) = "zero" foo (1:_ ) = "one" foo (_:xs) = foo xs foo ( []) = "end" 

前三个方程式全部检查列表是否是非空的(等等)。 但同样的事情检查三次是浪费。 幸运的是,编译器很容易将其优化为几个嵌套的caseexpression式。 在这种情况下,类似的东西

 foo xs = case xs of y:ys -> case y of 0 -> "zero" 1 -> "one" _ -> foo ys [] -> "end" 

这是相当不直观,但更有效。 因为编译器可以很容易地做这个转换,所以你不必担心它。 只需以最直观的方式编写模式匹配; 编译器非常善于重新sorting和重新排列,使其尽可能快。

聚变

用于列表处理的标准Haskell成语是将带有一个列表的函数链接在一起并产生一个新的列表。 规范的例子是

 map g . map f 

不幸的是,虽然懒惰保证跳过不必要的工作,但是中间表的所有分配和释放都会影响性能。 “融合”或“毁林”是编译器试图消除这些中间步骤的地方。

麻烦的是,这些function大部分是recursion的。 没有recursion,在内联中将所有函数压缩成一个大代码块,在其上运行简化器,生成真正最优的代码,而不需要中间列表。 但是由于recursion,这是行不通的。

您可以使用{-# RULE #-}编译指示来解决这个问题。 例如,

 {-# RULES "map/map" forall fg xs. map f (map g xs) = map (fg) xs #-} 

现在,每当GHC看到应用于map ,就会将其挤压在列表中一个单一的通道上,从而消除了中间列表。

麻烦的是,这只适用于map其次是map 。 还有许多其他的可能性 – map跟着filterfilter跟着map等。而不是手动编码解决每个他们的解决scheme,所谓的“stream融合”发明了。 这是一个更复杂的伎俩,我不会在这里描述。

它的长短是:这些都是程序员编写的特殊优化技巧。 GHC本身对融合一无所知; 它全部在列表库和其他容器库中。 那么什么样的优化取决于你的容器库的写法(或者更现实的说,你select使用哪个库)。

例如,如果你使用Haskell '98数组,不要期待任何forms的融合。 但是我明白, vector库具有广泛的融合function。 这些都是关于图书馆的。 编译器只是提供了RULES编译指示。 (顺便说一句,这是非常强大的function,作为一个图书馆作者,你可以用它来重写客户端代码!)


元:

  • 我同意“先编码,档次第二,优化第三”的人。

  • 我也同意这样的观点:“让一个给定的devise决策花费多less成本的思维模型是有用的”。

平衡所有的事情,所有的…

如果仅在一个地方使用let绑定v = rhs,那么即使rhs很大,也可以指望编译器将其内联。

这个例外(几乎不是当前问题的一个例子)是lambdas冒着工作重复的风险。 考虑:

 let v = rhs l = \x-> v + x in map l [1..100] 

那里面的内容将是危险的,因为一个(句法)使用将转化为99个额外的rhs评估。 但是,在这种情况下,你不可能手动内联它。 所以基本上你可以使用这个规则:

如果你考虑内联一个只出现一次的名字,那么编译器将会这样做。

作为一个推论,使用let绑定来分解一个长的声明(希望获得清晰)基本上是免费的。

这来自community.haskell.org/~simonmar/papers/inline.pdf,其中包含更多关于内联的信息。