Haskell 哈斯克尔写我的长度

Haskell 哈斯克尔写我的长度,haskell,Haskell,我正在写这一页 我理解每个函数的含义,看到一个函数可以用多种方式定义是很有趣的。然而,我开始想知道哪一个更快。我想它应该是Prelude中的length length [] = 0 length (x:xs) = 1 + length xs 但是,这比序曲中的长度慢得多 length [] = 0 length (x:xs) = 1 + length xs 在我的计算机上,Prelude中的length在0.37秒内返回[1..10^7]的长度。然而,上面定义的函数花费了15.26秒 我定

我正在写这一页

我理解每个函数的含义,看到一个函数可以用多种方式定义是很有趣的。然而,我开始想知道哪一个更快。我想它应该是
Prelude
中的
length

length [] = 0
length (x:xs) = 1 + length xs
但是,这比
序曲
中的
长度
慢得多

length [] = 0
length (x:xs) = 1 + length xs
在我的计算机上,
Prelude
中的
length
在0.37秒内返回
[1..10^7]
的长度。然而,上面定义的函数花费了15.26秒

我定义了自己的长度函数,它使用累加器。只花了8.99秒


我想知道为什么会出现这些巨大的差异?

前奏只是语义规范;它不限制实现。 发件人:

它构成了序曲的规范。许多定义都是为了清晰而不是为了效率而编写的,并且不需要按照此处所示的方式实现规范

在GHC的情况下,实际的
length
函数得到了高度优化。

当您在
Prelude
中说“
length
在0.37秒内返回…”时,您指的是哪个编译器?例如,如果您使用GHC,您可以看到实际实现与简单实现不同

length [] = 0
length (x:xs) = 1 + length xs
即:

length l = len l 0#
  where
    len :: [a] -> Int# -> Int
    len []     a# = I# a#
    len (_:xs) a# = len xs (a# +# 1#)
这段代码使用累加器,并通过使用非固定整数避免了大量未计算thunk的问题,即,此版本是高度优化的

为了说明“简单”版本的问题,考虑<代码>长度[1, 2, 3 ] < /C> >如何评估:

length [1, 2, 3]
  => 1 + length [2, 3]
  => 1 + (1 + length [3])
  => 1 + (1 + (1 + length []))
  => 1 + (1 + (1 + 0))
在真正需要结果之前,不会计算总和,因此您可以看到,当输入是一个巨大的列表时,您将首先在内存中创建一个巨大的总和,然后仅在真正需要其结果时对其进行计算

相比之下,优化版本的计算结果如下:

length [1, 2, 3]
  => len [1, 2, 3] 0#
  => len [2, 3] (1#)
  => len [3] (2#)
  => len [] (3#)
  => 3

i、 例如,“+1”立即完成。

您应该注意的两个步骤是:

  • 您是否正在运行编译的代码而不是ghci中的代码
  • 你在使用-O2标志吗
以下基准测试是在Criteria中完成的,并使用以下功能,同时需要使用
MagicHash
pragma和导入
GHC.Base

myLength1 :: [a] -> Int                                                          
myLength1 [] = 0                                                                 
myLength1 (x:xs) = 1 + myLength1 xs                                              

myLength2 :: [a] -> Int                                                          
myLength2 lst = len lst 0                                                        
  where                                                                          
    len :: [a] -> Int -> Int                                                     
    len [] n = n                                                                 
    len (_:xs) n = len xs (n+1)                                                  

myLength3                  :: [a] -> Int                                         
myLength3 l                =  len l 0#                                           
  where                                                                          
    len :: [a] -> Int# -> Int                                                    
    len []     a# = I# a#                                                        
    len (_:xs) a# = len xs (a# +# 1#)
基准测试的结果(在末尾完整显示,未使用TH
-O2
标志)如下所示:

              mean
length    :   5.4818 ms
myLength1 : 202.1552 ms
myLength2 : 236.3042 ms
myLength3 :   5.3630 ms
现在,让我们在编译时使用
-02
标志

              mean
length    :   5.2597 ms
myLength1 :   12.882 ms
myLength2 :   5.2026 ms
myLength3 :   5.6393 ms,
请注意,
myLength3
的长度没有变化,但其余两个长度变化很大。朴素的方法包含3个不同的系数,
myLength2
现在可以通过模拟前奏曲的长度与内置长度进行比较,但不使用拆箱

另外值得注意的是,取消整型的myLength3变化不大,在ghci中可能比MyLength1或2更好

完整代码位于:

编辑:一些不适合评论的进一步信息:

带有字母的ghc标志
-O2
表示“应用每一个非危险优化,即使这意味着编译时间显著延长。“如果这涉及到一些数据类型的拆箱,我不会感到惊讶。你可以找到不同标志的进一步解释。这里有一个更大的ghc 7.6.2标志列表,但解释可能简短而神秘


我不太熟悉解装箱和基本操作,它们的结果在这里是第三种,包括解装箱类型。你偶尔会在优化教程中发现它们。大多数时候,你不应该为它们烦恼,除非你真的需要每一克的性能,因为正如我们上面所说,它们通常只会在使用其他优化标志后设置一个常数差。

您的意思是
长度(x:xs)=1+length xs在您定义的第二行中?答案可能也会有帮助。@chris Yea,对不起。我会解决它。注意GHC本身能够解装箱——您实际上不需要编写解装箱版本,至少在现代GHC中是这样。特别是,
length=len0,其中{len::Int->[a]->Int;len a[]=a;len a(:xs)=len(a+1)xs}
编译为几乎相同的核心(如果您将其扩展为更像其他定义,它似乎编译为稍差的核心:-(但这只是每个列表的固定成本——您不会看到任何显著差异)。最大的区别是累加器。谢谢你的回答!我使用的是GHCi。嗯……似乎有很多事情超出了我的知识范围。使用未绑定累加器的效果是,即使没有优化,编译器也无法从中生成低效的代码。使用
myLength2
,你需要严格性分析器若要取消累加器的装箱,但这样做,您将得到相同的代码。谢谢您的回答!我实际上是一个初学者,到目前为止我只使用过GHCi…从未听说过02。O2与Int#有关吗?我以前从未见过Int#…@Tengu我编辑了我的答案,因为回复与评论不符,其他人可能也会受益。@Davorak Tha谢谢你的回答。我真的很感激你的回答,并且读了几遍,但对我来说似乎太难了。我会读你的回答,因为我在这方面有更多的知识。无论如何,谢谢!@Tengu我很高兴听到什么是最难理解的部分,所以下次我解释的时候,如果你有时间的话,可能会好一点帮我写下来。谢谢你的回答。这似乎超出了我的能力,所以我不会想太多…我一定会在我更熟悉Haskell的时候回到这里。无论如何,谢谢!