Haskell 在查找列表的最后一个但第二个元素时,为什么使用'last'是这些元素中最快的?

Haskell 在查找列表的最后一个但第二个元素时,为什么使用'last'是这些元素中最快的?,haskell,Haskell,下面给出了3个函数,用于查找列表中最后的第二个元素。使用的最后一个。init似乎比其他的要快得多。我似乎不明白为什么 为了进行测试,我使用了一个输入列表[1..100000000](1亿)。最后一个几乎立即运行,而其他的则需要几秒钟 ——慢 myButLast::[a]->a myButLast[x,y]=x myButLast(x:xs)=myButLast xs myButLast=错误“列表太短” --体面的 myButLast'::[a]->a myButLast'=(!!1)。颠倒 -

下面给出了3个函数,用于查找列表中最后的第二个元素。使用
的最后一个。init
似乎比其他的要快得多。我似乎不明白为什么

为了进行测试,我使用了一个输入列表
[1..100000000]
(1亿)。最后一个几乎立即运行,而其他的则需要几秒钟

——慢
myButLast::[a]->a
myButLast[x,y]=x
myButLast(x:xs)=myButLast xs
myButLast=错误“列表太短”
--体面的
myButLast'::[a]->a
myButLast'=(!!1)。颠倒
--快速
myButLast“”::[a]->a
myButLast''=最后一个。初始化

在研究速度和优化时,很容易理解。在里面 特别是,如果不提及 编译器版本和基准测试设置的优化模式。即便如此,现代 处理器是如此复杂,以神经网络为基础的分支预测功能,而不是 提到所有类型的缓存,因此,即使经过仔细设置,基准测试结果也会模糊不清

话虽如此

标杆管理是我们的朋友。 是一个提供高级基准测试工具的包。我很快起草了一份报告 这样的基准:

module Main where

import Criterion
import Criterion.Main

-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"

-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse

-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init

butLast2 :: [a] -> a
butLast2 (x :     _ : [ ] ) = x
butLast2 (_ : xs@(_ : _ ) ) = butLast2 xs
butLast2 _ = error "List too short"

setupEnv = do
  let xs = [1 .. 10^7] :: [Int]
  return xs

benches xs =
  [ bench "slow?"   $ nf myButLast   xs
  , bench "decent?" $ nf myButLast'  xs
  , bench "fast?"   $ nf myButLast'' xs
  , bench "match2"  $ nf butLast2    xs
  ]

main = defaultMain
    [ env setupEnv $ \ xs -> bgroup "main" $ let bs = benches xs in bs ++ reverse bs ]
如您所见,我添加了一个变量,该变量可以同时在两个元素上显式匹配,但除此之外 是相同的代码逐字逐句。我还反向运行基准测试,以便了解所产生的偏差 到缓存。所以,让我们跑去看看

% ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.6.5


% ghc -O2 -package criterion A.hs && ./A
benchmarking main/slow?
time                 54.83 ms   (54.75 ms .. 54.90 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 54.86 ms   (54.82 ms .. 54.93 ms)
std dev              94.77 μs   (54.95 μs .. 146.6 μs)

benchmarking main/decent?
time                 794.3 ms   (32.56 ms .. 1.293 s)
                     0.907 R²   (0.689 R² .. 1.000 R²)
mean                 617.2 ms   (422.7 ms .. 744.8 ms)
std dev              201.3 ms   (105.5 ms .. 283.3 ms)
variance introduced by outliers: 73% (severely inflated)

benchmarking main/fast?
time                 84.60 ms   (84.37 ms .. 84.95 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 84.46 ms   (84.25 ms .. 84.77 ms)
std dev              435.1 μs   (239.0 μs .. 681.4 μs)

benchmarking main/match2
time                 54.87 ms   (54.81 ms .. 54.95 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 54.85 ms   (54.81 ms .. 54.92 ms)
std dev              104.9 μs   (57.03 μs .. 178.7 μs)

benchmarking main/match2
time                 50.60 ms   (47.17 ms .. 53.01 ms)
                     0.993 R²   (0.981 R² .. 0.999 R²)
mean                 60.74 ms   (56.57 ms .. 67.03 ms)
std dev              9.362 ms   (6.074 ms .. 10.95 ms)
variance introduced by outliers: 56% (severely inflated)

benchmarking main/fast?
time                 69.38 ms   (56.64 ms .. 78.73 ms)
                     0.948 R²   (0.835 R² .. 0.994 R²)
mean                 108.2 ms   (92.40 ms .. 129.5 ms)
std dev              30.75 ms   (19.08 ms .. 37.64 ms)
variance introduced by outliers: 76% (severely inflated)

benchmarking main/decent?
time                 770.8 ms   (345.9 ms .. 1.004 s)
                     0.967 R²   (0.894 R² .. 1.000 R²)
mean                 593.4 ms   (422.8 ms .. 691.4 ms)
std dev              167.0 ms   (50.32 ms .. 226.1 ms)
variance introduced by outliers: 72% (severely inflated)

benchmarking main/slow?
time                 54.87 ms   (54.77 ms .. 55.00 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 54.95 ms   (54.88 ms .. 55.10 ms)
std dev              185.3 μs   (54.54 μs .. 251.8 μs)
看起来我们的“慢”版本一点也不慢!而且模式匹配的复杂性并不重要 添加任何内容。(我们看到连续两次运行
match2
I之间的速度略有加快,这归因于 缓存的影响。)

有一种方法可以获得更多的“科学”数据:我们可以查看 编译器查看代码的方式

检查中间结构是我们的朋友。 “核心”是GHC的内部语言。每个Haskell源文件在之前都简化为核心 转换为运行时系统要执行的最终功能图。如果我们看 在这个中间阶段,它将告诉我们
myButLast
butLast2
是等价的。信息技术 确实值得一看,因为在重命名阶段,所有漂亮的标识符都被随机损坏

% for i in `seq 1 4`; do echo; cat A$i.hs; ghc -O2 -ddump-simpl A$i.hs > A$i.simpl; done

module A1 where

-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"

module A2 where

-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse

module A3 where

-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init

module A4 where

butLast2 :: [a] -> a
butLast2 (x :     _ : [ ] ) = x
butLast2 (_ : xs@(_ : _ ) ) = butLast2 xs
butLast2 _ = error "List too short"

% ./EditDistance.hs *.simpl
(("A1.simpl","A2.simpl"),3866)
(("A1.simpl","A3.simpl"),3794)
(("A2.simpl","A3.simpl"),663)
(("A1.simpl","A4.simpl"),607)
(("A2.simpl","A4.simpl"),4188)
(("A3.simpl","A4.simpl"),4113)
似乎
A1
A4
最为相似。彻底检查将表明
A1
A4
中的代码结构相同。
A2
A3
相似也是合理的 因为两者都定义为两个函数的组合

如果要广泛检查
核心
输出,也可以使用
-dsuppress模块前缀
-dsuppress uniques
。它们使它更容易阅读

我们的敌人名单也很短。 那么,基准测试和优化会出什么问题呢

  • ghci
    ,专为交互式播放和快速迭代而设计,将Haskell源代码编译成某种字节码风格,而不是最终的可执行文件,并避免昂贵的优化,以利于更快地重新加载
  • 评测似乎是一个很好的工具,可以查看复杂程序的各个位和部分的性能,但它会严重破坏编译器的优化,结果会相差几个数量级。
    • 您的保护措施是使用自己的基准运行程序将每一小块代码作为单独的可执行文件进行分析
  • 垃圾收集是可调的。垃圾收集的延迟将以无法直接预测的方式影响性能
  • 正如我提到的,不同的编译器版本将构建不同性能的代码,因此在做出任何承诺之前,您必须知道代码用户可能会使用哪个版本来构建它,并以此为基准

这可能看起来很悲伤。但大多数情况下,这并不是Haskell程序员应该关心的事情。真实故事:我有一个朋友最近刚开始学习Haskell。他们编写了一个数值积分程序,但速度很慢。所以我们坐在一起,写下了算法的描述,包括图表和其他东西。当他们重新编写代码以与抽象描述保持一致时,它神奇地变得像猎豹一样快,而且内存也很薄。我们很快就计算出π。这个故事的寓意是什么?完美的抽象结构,你的代码就会自我优化。

init
已经过优化,以避免多次“解包”列表。@WillemVanOnsem但是为什么
myButLast
要慢得多呢?。它似乎没有解包任何列表,只是像
init
函数那样遍历它…@Ismor:is
[x,y]
(x:(y:[]))
的缩写,因此它解包外部cons,第二个cons,并检查第二个
cons
的尾部是否是
[]
。此外,第二个子句将在
(x:xs)
中再次解压列表。是的,解包是相当有效的,但当然,如果它经常发生,会减慢过程。从中看,优化似乎是
init
不会重复检查其参数是单例列表还是空列表。一旦递归开始,它只是假设第一个元素将被附加到递归调用的结果上。@WillemVanOnsem我认为解包可能不是这里的问题:GHC确实调用模式专门化,它应该自动为您提供优化版本的
myButLast
。我认为加速的罪魁祸首更可能是列表融合。信息量很大,在现阶段对我来说也有点难以承受。在本例中,我所做的所有“基准测试”都是针对1亿个项目列表运行所有功能,并注意到其中一个比另一个耗时更长。标准基准