【问题标题】:GHC - turning iterate into a tight loopGHC - 将迭代变成一个紧密的循环
【发布时间】:2017-09-27 14:32:17
【问题描述】:

我正在尝试学习/评估 Haskell,并且正在努力为一个简单的案例获得高效的可执行文件。 我正在使用的测试是 PRNG 序列(复制 PCG32 RNG)。我把它写成一个基本状态转换函数的迭代(我现在只看状态)。

{-# LANGUAGE BangPatterns #-}
import System.Environment (getArgs)
import Data.Bits
import Data.Word

iterate' f !x = x : iterate' f (f x)

main = print $ pcg32_rng 100000000

pcg32_random_r :: Word64 -> Word64 -> Word64
pcg32_random_r !i !state = state * (6364136223846793005 :: Word64) + (i .|. 1)
{-# INLINE pcg32_random_r #-}

pcg32_rng_s = iterate' (pcg32_random_r 1) 0

pcg32_rng n = pcg32_rng_s !! (n - 1)

我可以编译和运行该代码。它仍然使用比它应该使用的更多的内存,并且运行速度比 C 等价物慢 10 倍。主要问题似乎是迭代没有变成一个简单的循环。

让 GHC 在这里生成更快/更高效的代码我缺少什么?

编辑

这是我比较的 C 版本,它本质上捕获了我想要实现的目标。我尝试进行公平比较,但如果我遗漏了什么,请告诉我。

#include <stdio.h>
#include <stdint.h>

int main() {
  uint64_t oldstate,state;
  int i;

  for(i=0;i<100000000;i++) {
    oldstate = state;
    // Advance internal state
    state = oldstate * 6364136223846793005ULL + (1|1);
  }
  printf("%ld\n",state);
}

我最初尝试使用 Prelude iterate 函数,但这会导致延迟评估和堆栈溢出。 “terate”旨在解决这个问题。

我的下一步是尝试让 GHC 内联pcg32_random_r,这就是我对其添加严格性的地方,但这似乎还不够。当我查看 GHC 核心时,它不是内联的。

@WillemVanOnsem 我用perform确认结果与C相当,实际上pcg32_random_r函数是内联的。在这个阶段,我已经达到了对 Haskell 和 GHC 的掌握的极限。您能否详细说明为什么perform 表现更好以及如何决定何时使用什么?

这种转换是否可以由编译器自动执行,还是需要设计决策?

问最后一个问题的原因是,我希望将功能和实现选择(速度/空间权衡,...)分开,以最大限度地重用,我希望 Haskell 能在这方面帮助我。

【问题讨论】:

  • 你能展示 C 的等价物吗?它是否也将列表元素一一分配,还是将它们全部预先分配为数组?
  • 您是否分析并确定Data.List.iterate 比您在这里写的要慢?我认为 pcg32_random_r 的严格性足以避免累积 thunk 并且 iterate 的 Prelude 版本使用 foldr/build fusion 可能会通过 GHC 更好地优化
  • @cdk iterate 的问题在于传递给它的函数的严格性无关紧要。如果您丢弃未评估的结果元素,它总是会建立 thunk。这限制了它在您评估您考虑的部分结果的大部分元素的情况下的适用性。
  • @Carl 用户 luqui 曾经在 SO 上展示过如何通过将地图编写为折叠来克服这个问题,使用 seq 将映射与 (:) 节点创建联系起来,以便访问本身自动强制逐步评价。剩下的就是把 iterate 写成一个 map,iter f x = ys where ys = {- x : map f ys = -} x : foldr ((:) . (f $!)) undefined ys。用xs = iter (1:) [0 :: Int] 然后head (xs !! 3) 然后:sprint xs 进行测试时,似乎可以工作。
  • @WillNess 当然,您可以编写一个比iterate 更严格的函数。但是,如果您丢弃它产生的一些序列值,则没有任何东西可以传递给 iterate 以防止它建立 thunk。

标签: haskell ghc


【解决方案1】:

在我看来,问题更多在于您生成一个列表,然后从该列表中获取第 i 个元素。因此,您将展开该列表函数,并且每次构建一个新元素时,如果您需要在列表中进一步移动。

而不是构造这样的列表(它将构造新节点,并执行内存分配,并消耗大量内存)。您可以构造一个函数来执行给定函数n 次:

perform_n :: (a -> a) -> Int -> a -> a
perform_n !f = step
    where step !n !x | n <= 0 = x
                     | otherwise = step (n-1) (f x)

所以现在我们可以执行函数fn 次。因此,我们可以像这样重写它:

pcg32_rng n = perform_n (pcg32_random_r 1) (n-1) 0

如果我用ghc -O2 file.hs (GHC 8.0.2) 编译这个文件,用time 运行这个文件,我得到:

$ time ./file
2264354473547460187
0.14user 0.00system 0:00.14elapsed 99%CPU (0avgtext+0avgdata 3408maxresident)k
0inputs+0outputs (0major+161minor)pagefaults 0swaps

原始文件产生以下基准:

$ time ./file2
2264354473547460187
0.54user 0.00system 0:00.55elapsed 99%CPU (0avgtext+0avgdata 3912maxresident)k
0inputs+0outputs (0major+287minor)pagefaults 0swaps

编辑

正如@WillNess 所说,如果您不命名列表,则在运行时该列表将被垃圾收集:如果您通过列表进行处理,并且不保留对列表头部的引用,那么该头部可以一旦我们越过它就会被移除。

如果我们构建一个像这样的文件:

{-# LANGUAGE BangPatterns #-}
import System.Environment (getArgs)
import Data.Bits
import Data.Word

iterate' f !x = x : iterate' f (f x)

main = print $ pcg32_rng 100000000

pcg32_random_r :: Word64 -> Word64 -> Word64
pcg32_random_r !i !state = state * (6364136223846793005 :: Word64) + (i .|. 1)
{-# INLINE pcg32_random_r #-}

pcg32_rng n = iterate' (pcg32_random_r 1) 0 !! (n - 1)

我们得到:

$ time ./speedtest3
2264354473547460187
0.54user 0.01system 0:00.56elapsed 99%CPU (0avgtext+0avgdata 3908maxresident)k
0inputs+0outputs (0major+291minor)pagefaults 0swaps

虽然可以减少内存负担,但对时间影响不大。原因可能是使用列表元素会创建 cons 对象。所以我们做了很多打包和解包到列表中。这也会导致构建大量对象(和内存分配),这仍然会产生开销。

【讨论】:

  • 这与简单的 C 解决方案(即循环中的 state = state * ... + (i | 1),并使用 gcc -O2 编译)相比具有优势——两者在我的笔记本电脑上运行时间约为 120 毫秒——所以这可能是关于可实现的最佳性能。
  • @K.A.Buhr:当然可以找到一些额外的优化,但我想最重要的瓶颈可能已经修复:)。
  • 一个暂定的经验法则是"if you name it, it will stay"(在内存中)。所以也许像写pcg32_rng n = iterate' (pcg32_random_r 1) 0 !! (n - 1)(当然not定义pcg32_rng_s)这样简单的东西会产生同样的效果?你有兴趣检验这个假设吗?
  • @WillNess:它是0.54s。这是合乎逻辑的:如果你“展开”这个列表构造函数,你将继续构造列表节点。这就是时间开销的来源。内存负担会更小。但是列表节点中的打包和解包可能是时间开销的原因。
  • 所以它不起作用。嗯。可以想象,编译器可以完全避免 cons 单元的创建和销毁,在这种明显的情况下。
猜你喜欢
  • 1970-01-01
  • 2015-07-09
  • 1970-01-01
  • 1970-01-01
  • 2011-01-13
  • 2020-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多