繁体   English   中英

使用GHC最大化Haskell循环性能

[英]Maximizing Haskell loop performance with GHC

为了比较性能与GHC bug中缓慢的列表,我试图尽可能快地得到以下循环:

{-# LANGUAGE BangPatterns #-}

module Main (main) where

import Control.Monad
import Data.Word


main :: IO ()
main = do
  loop (maxBound :: Word32) $ \i -> do
    when (i `rem` 100000000 == 0) $
      print (fromIntegral i / fromIntegral (maxBound :: Word32))


loop :: Word32 -> (Word32 -> IO ()) -> IO ()
loop n f = go 0
  where
    go !i | i == n = return ()
    go !i          = f i >> go (i + 1)

ghc -O loop.hs编译。

但是,在我的计算机上运行它需要50秒 - 比同等的C程序慢10倍

#include "limits.h"
#include "stdint.h"
#include "stdio.h"

int main(int argc, char const *argv[])
{
  for (uint32_t i = 0; i < UINT_MAX; ++i)
  {
    if (i % 100000000 == 0) printf("%f\n", (float) i / (float) UINT_MAX );
  }
  return 0;
}

gcc -O2 -std=c99 -o testc test.c编译。


使用新发布的GHC 7.8或使用-O2不会改善性能。

但是,使用-fllvm标志进行编译(在任何一个ghc版本上)都提高了10倍的速度,使性能与C相提并论。

问题:

  1. 为什么GHC的本机代码对我的loop要慢得多?
  2. 有没有办法改善我的循环,以便它也很快没有-fllvm ,或者这已经是Word32最快的IO循环了?

我们来检查一下装配。 我稍微修改了主函数,使输出变得更清晰(但性能保持不变)。 我使用GHC 7.8.2和-O2。

main :: IO ()
main = do
  loop (maxBound :: Word32) $ \i -> do
    when (i `rem` 100000000 == 0) $
      putStrLn "foo"

有很多混乱,所以我尝试只包括有趣的部分:

Native Codegen

Main_zdwa_info:
.Lc3JD: /* check if there's enough space for stack growth */
    leaq -16(%rbp),%rax
    cmpq %r15,%rax
    jb .Lc3JO /* this jumps to some GC code that grows the stack, then
                 reenters the main closure */
.Lc3JP:
    movl $4294967295,%eax /* issue: loading the bound on every iteration */
    cmpq %rax,%r14
    jne .Lc3JB
.Lc3JC:
   /* Return from main. Code omitted */
.Lc3JB: /* test the index for modulus */
    movl $100000000,%eax /* issue: unnecessary moves */
    movq %rax,%rbx      
    movq %r14,%rax
    xorq %rdx,%rdx
    divq %rbx /* issue: doing the division (llvm and gcc avoid this) */
    testq %rdx,%rdx
    jne .Lc3JU
.Lc3JV: 
   /* do the printing. Code omitted. */
.Lc3JN:
   /* increment index and (I guess) restore registers messed up by the printing */
    movq 8(%rbp),%rax
    incq %rax  
    movl %eax,%r14d
    addq $16,%rbp
    jmp Main_zdwa_info
.Lc3JU:
    leaq 1(%r14),%rax   /*issue: why not just increment r14? */
    movl %eax,%r14d     
    jmp Main_zdwa_info

LLVM

 Main_zdwa_info:
/* code omitted: the same stack-checking stuff as in native */
.LBB1_1:
    movl    $4294967295, %esi /* load the bound */
    movabsq $-6067343680855748867, %rdi /*load a magic number for the modulus */
    jmp .LBB1_2
.LBB1_4:              
    incl    %ecx
.LBB1_2:  
    cmpq    %rsi, %rcx
    je  .LBB1_6 /* check bound */

    /* do the modulus with two multiplications, a shift and a magic number */
    /* note : gcc does the same reduction */ 
    movq    %rcx, %rax
    mulq    %rdi
    shrq    $26, %rdx
    imulq   $100000000, %rdx, %rax  
    cmpq    %rax, %rcx
    jne .LBB1_4 
    /* Code omitted: print, then return to loop beginning */
.LBB1_6:                       
    /* Code omitted: return from main */

意见

  • 两个程序集中都不存在IO开销。 零字节的RealWorld状态令牌显然不存在。

  • 与LLVM相比,本地codegen没有做太多的强度降低,LLVM很容易将模数转换为乘法,移位和幻数。

  • Native codegen在每次迭代时重做堆栈空间检查,而LLVM则不会。 然而,它似乎并不是一个重要的开销。

  • 本地codegen在循环和寄存器分配方面非常糟糕。 它会在寄存器周围进行混洗,并在每次迭代时加载绑定。 LLVM在整洁中发出与手写代码相当的代码。

至于你的问题:

有没有办法改善我的循环,以便它也很快没有-fllvm,或者这>已经是Word32上可以实现的最快的IO循环?

我认为,你可以做的最好的是减少手动强度,尽管我个人认为这个选项是不可接受的。 但是,在执行此操作后,您的代码仍然会显着变慢。 我还运行了以下简单的循环,它使用LLVM的速度是本机的两倍:

import Data.Word
main = go 0 where
    go :: Word32 -> IO ()
    go i | i == maxBound = return ()
    go i = go (i + 1)

罪魁祸首再次是不必要的寄存器重排和绑定加载。 除了切换到LLVM之外,没有任何方法可以解决这些低级问题。

一个简单的优化是使用Float division而不是默认的Double 只需编写一个便利函数来替换fromIntegral

w2f :: Word32 -> Float
w2f = fromIntegral

但是,像这样计算循环快得多:

main :: IO () 
main = forM_ [0, 100000000 .. mb] $ \i ->
    print (fromIntegral i / fromIntegral mb :: Float))
    where mb = maxBound :: Word32

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM