函数调用在Haskell中有多少开销?

use*_*556 17 performance haskell

我是Haskell的新手,我对函数调用的成本感到困惑,这对我来说似乎是完全不合理的,并且让我觉得我做的事情从根本上是错误的.

考虑以下Haskell代码:

module Main where
logistic x = 4.0*x*(1.0-x)

lg :: Double -> Int -> Double
lg !x 0 = x
lg !x !n = lg (logistic x) (n-1)

main = putStrLn $ show $ lg 0.7861 100000000
Run Code Online (Sandbox Code Playgroud)

用命令编译它

 ghc -O3 -XBangPatterns -o tsths tst.hs
Run Code Online (Sandbox Code Playgroud)

并运行它,我得到:

real    0m15.904s
user    0m15.853s
sys     0m0.016s
Run Code Online (Sandbox Code Playgroud)

如果不是调用函数,logistic而是计算内联表达式:

module Main where

lg :: Double -> Int -> Double
lg !x 0 = x
lg !x !n = lg (4.0*x*(1.0-x)) (n-1)

main = putStrLn $ show $ lg 0.7861 100000000
Run Code Online (Sandbox Code Playgroud)

执行时间变为:

real    0m0.838s
user    0m0.828s
sys     0m0.004s
Run Code Online (Sandbox Code Playgroud)

这与等效的C程序完全相同

#include <stdio.h>

int main() {
   int i, num=100000000;
   double x=0.7861;
   for (i=0; i<num; ++i)
      x *= 4.0*(1.0-x);
   printf("%lg\n", x);
}
Run Code Online (Sandbox Code Playgroud)

我做错了什么吗?

非常感谢.

Dan*_*her 21

这是GHC-7.4.1中的一个错误.查看生成的核心(只有函数的核心lg是重要的,从GHC-7.4.2,我们得到

Main.lg3 :: GHC.Types.Double
[GblId,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
         ConLike=False, Cheap=False, Expandable=False,
         Guidance=IF_ARGS [] 30 0}]
Main.lg3 = GHC.Float.$w$cfromRational Main.lg4 Main.lg2

Main.lg1 :: GHC.Types.Double
[GblId,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
         ConLike=False, Cheap=False, Expandable=False,
         Guidance=IF_ARGS [] 30 0}]
Main.lg1 = GHC.Float.$w$cfromRational Main.lg2 Main.lg2

Main.$wlg :: GHC.Prim.Double# -> GHC.Prim.Int# -> GHC.Prim.Double#
[GblId,
 Arity=2,
 Str=DmdType LL,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [0 30] 158 0}]
Main.$wlg =
  \ (ww_s1Oy :: GHC.Prim.Double#) (ww1_s1OC :: GHC.Prim.Int#) ->
    case ww1_s1OC of ds_Xvs {
      __DEFAULT ->
        case Main.lg3 of _ { GHC.Types.D# x_awJ ->
        case Main.lg1 of _ { GHC.Types.D# x1_awV ->
        letrec {
          $wlg1_X1PF [Occ=LoopBreaker]
            :: GHC.Prim.Double# -> GHC.Prim.Int# -> GHC.Prim.Double#
          [LclId, Arity=2, Str=DmdType LL]
          $wlg1_X1PF =
            \ (ww2_X1Pv :: GHC.Prim.Double#) (ww3_X1PA :: GHC.Prim.Int#) ->
              case ww3_X1PA of ds1_Xwr {
                __DEFAULT ->
                  $wlg1_X1PF
                    (GHC.Prim.*##
                       (GHC.Prim.*## x_awJ ww2_X1Pv) (GHC.Prim.-## x1_awV ww2_X1Pv))
                    (GHC.Prim.-# ds1_Xwr 1);
                0 -> ww2_X1Pv
              }; } in
        $wlg1_X1PF
          (GHC.Prim.*##
             (GHC.Prim.*## x_awJ ww_s1Oy) (GHC.Prim.-## x1_awV ww_s1Oy))
          (GHC.Prim.-# ds_Xvs 1)
        }
        };
      0 -> ww_s1Oy
    }
Run Code Online (Sandbox Code Playgroud)

两个顶级Doubles和一个体面的循环.

生成的GHC-7.4.1有点过于内向 - 快乐

Rec {
Main.$wlg [Occ=LoopBreaker]
  :: GHC.Prim.Double# -> GHC.Prim.Int# -> GHC.Prim.Double#
[GblId, Arity=2, Str=DmdType LL]
Main.$wlg =
  \ (ww_s1NS :: GHC.Prim.Double#) (ww1_s1NW :: GHC.Prim.Int#) ->
    case ww1_s1NW of ds_Xvb {
      __DEFAULT ->
        case GHC.Float.$wfromRat'' (-1021) 53 Main.logistic4 Main.logistic2
        of ww2_a1Mt { __DEFAULT ->
        case GHC.Float.$wfromRat'' (-1021) 53 Main.logistic2 Main.logistic2
        of ww3_X1Nq { __DEFAULT ->
        Main.$wlg
          (GHC.Prim.*##
             (GHC.Prim.*## ww2_a1Mt ww_s1NS) (GHC.Prim.-## ww3_X1Nq ww_s1NS))
          (GHC.Prim.-# ds_Xvb 1)
        }
        };
      0 -> ww_s1NS
    }
end Rec }
Run Code Online (Sandbox Code Playgroud)

fromRational在每次迭代中给你两次调用.

现在,这fromRational是一个相当复杂的功能.它仍然相当缓慢,尽管在7.2系列中实现了更快的实现,因此这些调用会伤害大量时间.

使用类型签名,没有Rational产生顶级常量,只有Double常量,然后使用这些常量,这当然不包括无偿的减速.


Vla*_*ill 11

正如Dan Burton所建议的,它实际上是多态函数的开销,因为GHC推断类型logistic :: Fractional a => a -> a.如果明确指定类型,通常会启用更好的检查和更好的优化.我认为明确指定函数类型是一种好习惯.

如果你想拥有多态类型的函数但是在特定用法的情况下具有单态调用的全速,你可以使用SPECIALIZEpragma,但我相信这是GHC特定的.

{-# LANGUAGE BangPatterns #-}
module Main where
logistic :: Fractional a => a -> a
{-# SPECIALISE logistic :: Double -> Double #-}
logistic x = 4.0*x*(1.0-x)

lg :: Double -> Int -> Double
lg !x 0 = x
lg !x !n = lg (logistic x) (n-1)

main = putStrLn $ show $ lg 0.7861 100000000
Run Code Online (Sandbox Code Playgroud)

另请注意,您可以LANGUAGE在文件开头指定pragma以启用bang模式,而不需要在命令行上启用它们.

我的机器上的时间对于原始时间是21秒,对于显式类型是0.67秒,对于specialize来说是0.7秒(这基本上是相同的).

我相信专门调用的开销非常小,因为它只是一串指令,无论如何都会被内联,但是多态函数会导致调用.尽管多态性,但GHC不能内联,这很奇怪.


Dan*_*ton 9

添加类型签名logistic,您将看到加速.请允许我使用CPP来证明其差异.

bash> cat tst.hs
module Main where

#if defined(SIG)
logistic :: Double -> Double
#endif
logistic x = 4.0*x*(1.0-x)

lg :: Double -> Int -> Double
lg !x 0 = x
lg !x !n = lg (logistic x) (n-1)

main = putStrLn $ show $ lg 0.7861 100000000


bash> ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.4.1
Run Code Online (Sandbox Code Playgroud)

如果编译时未定义SIG(排除类型签名):

bash> ghc -O3 -XBangPatterns -XCPP -o tsths tst.hs
[1 of 1] Compiling Main             ( tst.hs, tst.o )
Linking tsths ...


bash> time ./tsths
0.34209286442469333

real    0m13.187s
user    0m13.177s
sys 0m0.008s
Run Code Online (Sandbox Code Playgroud)

现在让我们使用SIG定义编译,以便包含签名:

bash> rm tsths *.o *.hi


bash> ghc -O3 -XBangPatterns -XCPP -DSIG -o tsths tst.hs
[1 of 1] Compiling Main             ( tst.hs, tst.o )
Linking tsths ...
bash> time ./tsths
0.34209286442469333

real    0m0.464s
user    0m0.440s
sys 0m0.020s
Run Code Online (Sandbox Code Playgroud)

不确定为什么GHC没有签名就不优化它; 单态限制应该限制它Double -> Double无论如何.

  • BTW,`logistic`受功能绑定约束,因此MR不适用.但是,GHC生成一个专门的版本(GHC> = 6.12.3测试). (3认同)