GHC - 将迭代转变为紧密循环

时间:2017-09-27 14:32:18

标签: haskell

我正在尝试学习/评估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函数,但这会导致延迟评估和堆栈溢出。 “teterate”旨在解决这个问题。

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

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

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

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

1 个答案:

答案 0 :(得分:5)

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

而不是构造这样的列表(它将构造新的节点,并执行内存分配,并消耗大量内存)。您可以构造一个将执行给定函数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)

现在我们可以执行f n次函数了。因此我们可以重写它:

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 对象。所以我们做了大量的打包和打包到列表中。这也导致构造了许多仍然产生开销的对象(和内存分配)。