首先,我认为您不想使用列表。您的许多算法都依赖于计算长度,这很糟糕。您可能需要考虑 vector 包,与列表的 O(n) 相比,它会给您 O(1) 的长度。向量的内存效率也更高,尤其是当您可以使用 Unboxed 或 Storable 变体时。
话虽如此,您确实需要在代码中考虑遍历和使用模式。如果 Haskell 的列表可以按需生成并使用一次,则非常有效。这意味着您不应保留对列表的引用。像这样的:
average xs = sum xs / length xs
要求将整个列表保留在内存中(通过sum 或length)直到两个遍历都完成。如果你可以一步完成你的列表遍历,那效率会高很多。
当然,您可能仍然需要保留列表,例如检查所有元素是否相等,如果不相等,则对数据执行其他操作。在这种情况下,对于任何大小的列表,您可能最好使用更紧凑的数据结构(例如向量)。
现在这已经不成问题了,下面来看看这些函数中的每一个。在我展示核心的地方,它是用ghc-7.0.3 -O -ddump-simpl 生成的。此外,当使用 -O0 编译时,不要费心判断 Haskell 代码的性能。使用您实际用于生产代码的标志编译它,通常至少 -O 和其他选项。
解决方案 0
allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)
GHC 生产这个核心:
Test.allTheSame
:: forall a_abG. GHC.Classes.Eq a_abG => [a_abG] -> GHC.Bool.Bool
[GblId,
Arity=2,
Str=DmdType LS,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
ConLike=True, Cheap=True, Expandable=True,
Guidance=IF_ARGS [3 3] 16 0}]
Test.allTheSame =
\ (@ a_awM)
($dEq_awN :: GHC.Classes.Eq a_awM)
(xs_abH :: [a_awM]) ->
case xs_abH of _ {
[] ->
GHC.List.tail1
`cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
:: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
: ds1_axJ xs1_axK ->
letrec {
go_sDv [Occ=LoopBreaker] :: [a_awM] -> GHC.Bool.Bool
[LclId, Arity=1, Str=DmdType S]
go_sDv =
\ (ds_azk :: [a_awM]) ->
case ds_azk of _ {
[] -> GHC.Bool.True;
: y_azp ys_azq ->
case GHC.Classes.== @ a_awM $dEq_awN y_azp ds1_axJ of _ {
GHC.Bool.False -> GHC.Bool.False; GHC.Bool.True -> go_sDv ys_azq
}
}; } in
go_sDv xs1_axK
}
实际上,这看起来很不错。它会产生一个空列表的错误,但这很容易解决。这是case xs_abH of _ { [] ->。在这个 GHC 执行了 worker/wrapper 转换之后,递归的 worker 函数就是 letrec { go_sDv 绑定。工人检查它的论点。如果[],则到达列表末尾并返回True。否则,它将剩余元素的头部与第一个元素进行比较,然后返回 False 或检查列表的其余部分。
其他三个功能。
-
map 完全融合了
并且不分配临时
列表。
- 靠近定义的顶部
注意
Cheap=True 声明。
这意味着 GHC 认为
功能“便宜”,因此
内联的候选人。打电话
站点,如果是具体的参数类型
可以确定,GHC大概会
内联allTheSame 并生成一个
非常紧的内环,完全
绕过Eq 字典
查找。
- 工作函数是
尾递归。
结论:非常强大的竞争者。
解决方案 1
allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)
即使不看核心,我也知道这不会那么好。该列表不止一次被遍历,首先是length xs,然后是length $ takeWhile。不仅你有多次遍历的额外开销,这意味着列表必须在第一次遍历后保留在内存中并且不能被 GC'd。对于一个大列表,这是一个严重的问题。
Test.allTheSame'
:: forall a_abF. GHC.Classes.Eq a_abF => [a_abF] -> GHC.Bool.Bool
[GblId,
Arity=2,
Str=DmdType LS,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
ConLike=True, Cheap=True, Expandable=True,
Guidance=IF_ARGS [3 3] 20 0}]
Test.allTheSame' =
\ (@ a_awF)
($dEq_awG :: GHC.Classes.Eq a_awF)
(xs_abI :: [a_awF]) ->
case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
case GHC.List.$wlen
@ a_awF
(GHC.List.takeWhile
@ a_awF
(let {
ds_sDq :: a_awF
[LclId, Str=DmdType]
ds_sDq =
case xs_abI of _ {
[] -> GHC.List.badHead @ a_awF; : x_axk ds1_axl -> x_axk
} } in
\ (ds1_dxa :: a_awF) ->
GHC.Classes.== @ a_awF $dEq_awG ds1_dxa ds_sDq)
xs_abI)
0
of ww1_XCn { __DEFAULT ->
GHC.Prim.==# ww_aC6 ww1_XCn
}
}
看核心并不能说明太多。但是,请注意以下几行:
case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
case GHC.List.$wlen
这是列表遍历发生的地方。第一个获取外部列表的长度并将其绑定到ww_aC6。第二个获取内部列表的长度,但直到底部附近才发生绑定,在
of ww1_XCn { __DEFAULT ->
GHC.Prim.==# ww_aC6 ww1_XCn
长度(Ints)可以拆箱并通过 primop 进行比较,但在引入开销之后,这是一个小小的安慰。
结论:不好。
解决方案 2
allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
| n == 0 = False
| n == 1 = True
| n == 2 = xs !! 0 == xs !! 1
| otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
where n = length xs
这和解决方案1有同样的问题。列表被遍历了多次,不能被GC'd。不过这里更糟,因为现在为每个子列表计算长度。我希望这在任何显着大小的列表中都具有最差的性能。另外,当您期望列表很大时,为什么要使用 1 和 2 元素的特殊情况列表?
结论:别想了。
解决方案 3
allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
| n == 0 = False
| n == 1 = True
| n == 2 = xs !! 0 == xs !! 1
| n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
| otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
where n = length xs
split = splitAt (n `div` 2) xs
这与解决方案2有相同的问题。即length多次遍历列表。我不确定分而治之的方法是解决这个问题的好选择,它最终可能比简单的扫描花费更长的时间。不过,这取决于数据,值得测试。
结论:也许,如果您使用不同的数据结构。
解决方案 4
allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)
这基本上是我的第一个想法。让我们再次检查内核。
Test.allTheSame''''
:: forall a_abC. GHC.Classes.Eq a_abC => [a_abC] -> GHC.Bool.Bool
[GblId,
Arity=2,
Str=DmdType LS,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
ConLike=True, Cheap=True, Expandable=True,
Guidance=IF_ARGS [3 3] 10 0}]
Test.allTheSame'''' =
\ (@ a_am5)
($dEq_am6 :: GHC.Classes.Eq a_am5)
(xs_alK :: [a_am5]) ->
case xs_alK of _ {
[] ->
GHC.List.tail1
`cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
:: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
: ds1_axJ xs1_axK ->
GHC.List.all
@ a_am5
(\ (ds_dwU :: a_am5) ->
GHC.Classes.== @ a_am5 $dEq_am6 ds_dwU ds1_axJ)
xs1_axK
}
好吧,还不错。与解决方案 1 一样,这将在空列表上出错。列表遍历隐藏在GHC.List.all 中,但它可能会在调用站点扩展为好的代码。
判决:另一个强有力的竞争者。
因此,在所有这些之间,我希望解决方案 0 和 4 是唯一值得使用的列表,它们几乎相同。在某些情况下,我可能会考虑选项 3。
编辑:在这两种情况下,空列表上的错误都可以像@augustss 的回答一样简单地修复。
下一步是使用criterion 进行一些时间分析。