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


183

GHC可以执行很多优化,但是我不知道它们全都有,也不知道它们在多大程度上会在什么情况下执行。

我的问题是:我希望每次或几乎都会应用哪些转换?如果我查看一段要经常执行(评估)的代码,而我的第一个念头是“嗯,也许我应该优化它”,在这种情况下,我的第二个念头应该是:“甚至不要考虑它, GHC收到了这个?”

我正在阅读论文《流融合:从列表到流再到什么都没有》,而他们使用的将列表处理重写为另一种形式的技术对我来说是新颖的,GHC的常规优化然后可以将其可靠地优化为简单的循环。我如何知道自己的程序何时可以进行这种优化?

《 GHC手册》中有一些信息,但这只是回答问题的一部分。

编辑:我开始赏金。我想要的是较低级转换的列表,例如lambda / let / case-floating,type / constructor / function参数专门化,严格性分析和拆箱,worker / wrapper以及我遗漏的其他重要GHC所做的其他事情,以及输入和输出代码的说明和示例,以及理想情况下总效果超过其各个部分之和的情况的说明。理想情况下,提到何时不进行转换发生。我不希望每个转换都有新颖的解释,只要大的图景就可以用几个句子和内联的单行代码示例就足够了(或者链接,如果不是到二十页的科学论文)。最后清除。我希望能够看一段代码,并能够很好地猜测它是否可以编译成一个紧密的循环,或者为什么不编译,或者我必须进行更改以使其得以实现。(我对诸如流融合之类的大型优化框架不那么感兴趣(我只是读过一篇文章),而对编写这些框架的人所拥有的知识则没有那么大的兴趣。)


10
这是一个最值得的问题。写一个有价值的答案很棘手。
MathematicalOrchid

1
一个真正好的起点是:aosabook.org/en/ghc.html
加布里埃尔·冈萨雷斯

7
在任何语言中,如果您的第一个念头是“也许我应该优化它”,那么您的第二个念头应该是“我会首先介绍它”。
约翰L

4
虽然您所追求的知识是有帮助的,所以这仍然是一个好问题,但我认为尝试进行尽可能少的优化会更好地为您服务。写下您的意思,只有在明显需要考虑然后为了性能而使代码不那么直接时,才写您的意思。与其查看代码并认为“将要频繁执行的代码,也许我应该对其进行优化”,不如只是当您观察到代码运行太慢时,您会认为“我应该找出经常执行的代码并对其进行优化”。 。
2012年

14
我完全希望这部分内容能引起人们的劝告,以“描述它”!:)。但是我想硬币的另一面是,如果我对其进行分析并且速度很慢,也许我可以重写它,或者只是将其调整为仍然处于较高水平的形式,但是GHC可以更好地进行优化,而不是自己亲自对其进行优化?这需要相同的知识。如果我一开始就有这些知识,那么我本可以为自己节省一个编辑配置文件周期。
glaebhoerl 2012年

Answers:


110

此GHC Trac页面也很好地解释了通行证。该页面说明了优化顺序,但是,与大多数Trac Wiki一样,它已经过时了。

对于特定细节,最好的办法可能是查看特定程序的编译方式。查看正在执行哪些优化的最佳方法是使用该-v标志详细编译程序。以我可以在计算机上找到的第一批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:,所有优化阶段都在这里进行,我们看到了很多。

首先,Simplifier在几乎所有阶段之间运行。这使得编写许多通行证变得容易得多。例如,当实施许多优化时,他们只需创建重写规则即可传播更改,而不必手动进行。简化程序包含许多简单的优化,包括内联和融合。我知道的这一主要限制是,GHC拒绝内联递归函数,并且必须正确命名事物才能使融合工作。

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

  • 专攻

    专业化的基本思想是通过识别调用函数的位置并创建非多态的函数版本来消除多态和重载-它们特定于调用它们的类型。您还可以告诉编译器使用SPECIALISE编译指示来执行此操作。例如,采用阶乘函数:

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

    由于编译器不知道要使用的乘法的任何属性,因此它根本无法优化它。但是,如果看到它用在上Int,它现在可以创建一个新版本,只是类型有所不同:

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

    接下来,可以触发下面提到的规则,最终您可以对unboxed进行处理Int,这比原始操作要快得多。观察专业化的另一种方法是在类型类字典和类型变量上的部分应用。

    此处的源中包含大量注释。

  • 浮出

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

    其基本思想是将不应重复的计算移出函数。例如,假设我们有:

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

    在上述lambda中,每次调用该函数时,y都会重新计算一次。浮出的更好的功能是

    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之间的绑定没有浮动。例如,这不会发生:

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

    正在去

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

    引用源代码,

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

    例如,假设我们有以下表达式:

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

    如果v计算为False,则通过分配x,这大概是一个很大的负担,我们浪费了时间和空间。向内浮动可解决此问题,产生此问题:

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

    ,随后被的简化器替换为

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

    尽管涵盖了其他主题,但本文还是做了相当清晰的介绍。请注意,尽管有它们的名字,但由于两个原因,浮动和浮动不会陷入无限循环:

    1. 浮动中的casefloat 进入语句,而浮动中处理函数。
    2. 有固定的传递顺序,因此它们不应无限交替。

  • 需求分析

    需求分析或严格性分析不是一种转变,而是一种信息收集过程,顾名思义。编译器会找到始终评估其参数(或至少其中一些参数)的函数,并使用按值调用而不是按需调用来传递这些参数。由于您可以逃避重击的开销,因此通常更快。Haskell中的许多性能问题都是由于此传递失败或代码不够严格而引起的。一个简单的例子是使用之间的差foldrfoldlfoldl'求和一个整数列表-由于严格性,第一个导致堆栈溢出,第二个导致堆溢出,最后一个运行良好。这可能是所有这些中最容易理解和最好记录的。我相信多态和CPS代码通常会克服这一点。

  • 工人包装纸绑定

    worker / wrapper转换的基本思想是在一个简单的结构上进行紧密的循环,并在末端进行往返于该结构的转换。例如,使用此函数来计算数字的阶乘。

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

    使用IntGHC中的定义,我们有

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

    注意代码在I#s中如何覆盖?我们可以这样删除它们:

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

    尽管SpecConstr也可以完成此特定示例,但worker / wrapper转换在其可以执行的操作中非常通用。

  • 普通子表达式

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

    fib x + fib x

    进入

    let fib_x = fib x in fib_x + fib_x

    从而将计算量减少了一半。不幸的是,这有时会妨碍其他优化。另一个问题是这两个表达式必须位于同一位置,并且在语法上必须相同,而不是按值相同。例如,如果没有大量内联,CSE将不会在以下代码中触发:

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

    但是,如果通过llvm进行编译,则由于其“全局值编号”传递,您可能会合并其中的一部分。

  • 解放案

    除了可能导致代码爆炸之外,这似乎是一个记录非常详尽的转换。这是我发现的小文档的重新格式化(略有重写)的版本:

    该模块遍历Core,并寻找case自由变量。判据是:如果case到递归调用的路由上有一个free变量,则递归调用将被展开代替。例如,在

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

    内部f被替换。使

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

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

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

    这是更好的代码,因为a它在内部是自由的letrec,而不需要从进行投影v。请注意,它处理自由变量,这与SpecConstr不同,SpecConstr处理已知形式的参数

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

  • SpecConstr-这样可以转换程序

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

    进入

    f_Left x y = somethingComplicated1
    f_Right x y = 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

    请注意,该程序现在速度更快,因为我们没有反复对列表的开头进行装箱和拆箱。还请注意,内联至关重要,因为它可以实际使用更高效的新定义,并使递归定义更好。

    SpecConstr由许多启发式方法控制。本文中提到的是这样的:

    1. Lambda是显式的,而Arity是a
    2. 右侧是“足够小”,由标记控制。
    3. 该函数是递归的,并且在右侧使用了专门化的调用。
    4. 该函数的所有参数都存在。
    5. 至少一个自变量是构造函数应用程序。
    6. 该参数在函数中的某处进行了案例分析。

    但是,试探法几乎可以肯定已经改变。实际上,本文提到了另一种第六种启发式方法:

    专业上的说法x只有x由审查case,而不是传递给普通函数,或返回结果的一部分。

这是一个非常小的文件(12行),因此可能没有触发那么多优化(尽管我认为它可以全部完成)。这也没有告诉您为什么选择这些通行证以及为什么将其按顺序排列。


现在我们到了某个地方!评论:您似乎在“专业化”部分的句子很短。我看不出浮动的意义:它是做什么用的?它如何决定是浮动还是浮动(为什么不进入循环)?我有个印象,从地方,GHC没有做CSE 可言,但显然这是错误的。我觉得我迷失了细节,而不是看到大局...这个话题比我想象的还要复杂。也许我的问题是不可能的,只有大量的经验或自己从事GHC的工作,否则无法获得这种直觉?
glaebhoerl 2012年

好吧,我不知道,但是我从未从事过GHC工作,因此您必须能够获得一些直觉。
gereeter 2012年

我解决了您提到的问题。
gereeter 2012年

1
另外,关于大局,我认为确实没有。当我想猜测将要执行的优化时,我会核对清单。然后我再做一次,看看每遍都将如何改变事情。然后再次。本质上,我在玩编译器。我所知道的唯一真正具有“全局”的优化方案是超级编译。
gereeter

1
您所说的“必须正确命名才能使融合正常工作”是什么意思?
文森特·贝格拉

65

懒惰

这不是“编译器优化”,但这是语言规范所保证的,因此您始终可以指望它的发生。本质上,这意味着直到您对结果“做某事”,才可以执行工作。(除非您执行以下其中一项操作来故意关闭惰性功能。)

显然,这本身就是一个完整的主题,因此SO已经对此有很多疑问和答案。

在我有限的经验,使您的代码太懒或太严具有极大较大的性能损失(时间空间)比任何其他的东西,我要说说...

严格度分析

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

显然,要解决的问题是编译器无法始终检测何时可以使某些事情变得严格。有时您需要给编译器一些提示。(我不知道有什么简单的方法可以确定严格性分析是否已经完成了您认为的工作,而不是通过核心输出。)

内联

如果您调用一个函数,并且编译器可以告诉您要调用的函数,则它可能会尝试“内联”该函数-即用该函数本身的副本替换该函数调用。函数调用的开销通常很小,但是内联通常会使其他优化得以实现,否则本来就不会发生,因此内联可以是一个很大的胜利。

仅当函数“足够小”时(或添加专门要求内联的编译指示),才对函数进行内联。同样,仅当编译器可以告诉您要调用的函数时,才可以内联函数。编译器无法分辨的主要方式有两种:

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

  • 如果您要调用的函数是类方法并且编译器不知道所涉及的类型。例如,在sum编译函数时,编译器无法内联+函数,因为它sum适用于几种不同的数字类型,每种数字类型具有不同的+函数。

在后一种情况下,您可以使用{-# SPECIALIZE #-}编译指示来生成功能的版本,这些版本被硬编码为特定类型。例如,{-# SPECIALIZE sum :: [Int] -> Int #-}sum为该Int类型编译一个硬编码的版本,这意味着+可以内嵌在该版本中。

不过请注意,sum只有在编译器可以告知我们正在使用时,才会调用我们的新特殊功能Int。否则,sum将调用原始的多态。同样,实际的函数调用开销非常小。内联可以实现的其他优化是有益的。

常见子表达式消除

如果某个代码块两次计算出相同的值,则编译器可以将其替换为同一计算的单个实例。例如,如果您这样做

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

那么编译器可能会对此进行优化

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

您可能希望编译器将始终执行此操作。但是,显然在某些情况下,这可能会导致性能变差,而不是更好,因此GHC并不总是这样做。坦白说,我不太了解这背后的细节。但最重要的是,如果此转换对您来说很重要,那么手动进行并不难。(如果不重要,为什么还要担心呢?)

案例表达

考虑以下:

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

前三个方程式均检查列表是否为非空(以及其他内容)。但是三次检查同一件事是浪费的。幸运的是,编译器很容易将其优化为几个嵌套的case表达式。在这种情况下,

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

这相当不直观,但效率更高。由于编译器可以轻松进行此转换,因此您不必担心。只需以最直观的方式编写模式匹配即可;编译器非常擅长重新排序和重新安排它,以使其尽可能快。

融合

用于列表处理的标准Haskell习惯用法是将采用一个列表并生成新列表的函数链接在一起。典型的例子是

map g . map f

不幸的是,尽管懒惰保证跳过不必要的工作,但是中间列表树液性能的所有分配和取消分配。编译器将尝试消除这些中间步骤,而采用“融合”或“砍伐森林”的方法。

麻烦的是,这些功能大多数都是递归的。没有递归,将内联将所有功能压缩到一个大代码块中,在其上运行简化程序并生成没有中间列表的真正最佳代码,这将是一个基本练习。但是由于递归,这是行不通的。

您可以使用{-# RULE #-}编译指示来修复某些问题。例如,

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

现在,每当GHC map应用于时map,它就会将其压缩成一个遍历列表,从而消除中间列表。

麻烦的是,这仅适用mapmap。还有许多其他可能性- map其次是filterfilter其次是map,等等。不是为每个解决方案手动编写解决方案,而是发明了所谓的“流融合”。这是一个更复杂的把戏,在此不再赘述。

它的长短是:这些都是程序员编写的特殊优化技巧。GHC本身对融合一无所知。全部在列表库和其他容器库中。因此,进行哪种优化取决于您的容器库的编写方式(或更现实的是,您选择使用哪些库)。

例如,如果您使用Haskell '98阵列,请不要期望任何形式的融合。但我知道该vector库具有广泛的融合功能。都是关于库的;编译器仅提供RULES编译指示。(顺便说一下,这是非常强大的。作为库作者,您可以使用它来重写客户端代码!)


元:

  • 我同意人们所说的“编码优先,配置文件第二,优化第三”的观点。

  • 我也同意人们的看法,“对于给定的设计决定要花多少成本,建立一个心理模型很有用”。

在所有事物之间保持平衡...


9
it's something guaranteed by the language specification ... work is not performed until you "do something" with the result.- 不完全是。语言规范承诺非严格的语义;它不会保证是否进行多余的工作。
丹·伯顿

1
@丹伯顿当然。但是用几句话很难解释。此外,由于GHC几乎是现存的唯一Haskell实现,因此GHC懒惰这一事实对大多数人来说已经足够了。
MathematicalOrchid

@MathematicalOrchid:投机性评估是一个有趣的反例,尽管我同意对于初学者来说可能太多了。
本·米尔伍德

5
关于CSE:我的印象是,它几乎从未做过,因为它会引入不必要的共享,从而导致空间泄漏。
约阿希姆·布赖特纳

2
很抱歉(a)之前未回复,并且(b)不接受您的回答。这很长很令人印象深刻,但没有涵盖我想要的领域。我想要的是一个较低级转换的列表,例如lambda / let / case-floating,type / constructor / function参数专门化,严格性分析和拆箱(您提到),worker / wrapper以及GHC所做的其他任何事情带有输入和输出代码的说明和示例,理想情况下还包括它们的组合效果以及进行转换的示例。我想我应该赏金吗?
glaebhoerl 2012年

8

如果仅在一个地方使用let绑定v = rhs,则即使rhs很大,也可以依靠编译器对其进行内联。

例外(在当前问题的上下文中几乎不是一个例外)是lambda冒着重复工作的风险。考虑:

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

内联v会很危险,因为一个(语法上)使用会转化为rhs的99个额外评估。但是,在这种情况下,您也不太可能希望手动内联它。因此,基本上可以使用以下规则:

如果您考虑内联一个仅出现一次的名称,则编译器还是会这样做。

作为一个必然的推论,使用let绑定简单地分解一个长语句(希望获得清晰度)基本上是免费的。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.