newtypes比枚举更快吗?

时间:2012-10-12 20:03:24

标签: haskell ghc

根据this article

  

就GHC而言,枚举不算作单构造函数类型,因此当用作严格构造函数字段或严格函数参数时,它们不会受益于解包。这是GHC的一个缺陷,但它可以解决。

而是推荐使用newtypes。但是,我无法使用以下代码验证这一点:

{-# LANGUAGE MagicHash,BangPatterns #-}
{-# OPTIONS_GHC  -O2 -funbox-strict-fields -rtsopts -fllvm -optlc --x86-asm-syntax=intel #-}
module Main(main,f,g)
where       
import GHC.Base  
import Criterion.Main

data D = A | B | C
newtype E = E Int deriving(Eq)

f :: D -> Int#
f z | z `seq` False = 3422#
f z = case z of
  A -> 1234#
  B -> 5678#
  C -> 9012#

g :: E -> Int#
g z | z `seq` False = 7432#
g z = case z of
  (E 0) -> 2345#
  (E 1) -> 6789#
  (E 2) -> 3535#

f' x = I# (f x)
g' x = I# (g x)

main :: IO ()
main = defaultMain [ bench "f" (whnf f' A) 
                   , bench "g" (whnf g' (E 0)) 
                   ]

查看程序集时,枚举D的每个构造函数的标记实际上已解压缩并直接在指令中进行硬编码。此外,函数f缺少错误处理代码,比g快10%以上。在更现实的情况下,我在将枚举转换为新类型后也经历了减速。谁能给我一些关于此的见解?感谢。

2 个答案:

答案 0 :(得分:18)

这取决于用例。对于您拥有的功能,预计枚举的性能会更好。基本上,D的三个构造函数变为IntInt#当严格性分析允许时,GHC知道它静态地检查了参数只能有三个值中的一个0#, 1#, 2#,所以它不需要插入f的错误处理代码。对于E,没有给出三个值中只有一个可能的静态保证,因此需要为g添加错误处理代码,这会显着降低速度。如果您更改g的定义,以便最后一个案例变为

E _ -> 3535#

差异完全消失或几乎完全消失(我对f仍然有1% - 2%更好的基准,但我还没有做足够的测试来确定这是真正的差异还是基准测试的工件)。

但这不是维基页面所讨论的用例。它所讨论的是当类型是其他数据的一个组件时,将构造函数解压缩到其他构造函数中,例如。

data FooD = FD !D !D !D

data FooE = FE !E !E !E

然后,如果使用-funbox-strict-fields进行编译,则可以将三个Int#解压缩到FooE的构造函数中,因此您基本上可以获得等效的

struct FooE {
    long x, y, z;
};

虽然FooD的字段具有多构造函数类型D,但无法解压缩到构造函数FD (1)中,因此基本上给你

struct FooD {
    long *px, *py, *pz;
}

这显然会产生重大影响。

我不确定单构造函数参数的情况。对于包含数据的类型(如元组),这有明显的优势,但我不知道这将如何应用于普通枚举,你只有case并且拆分一个worker而一个包装器没有任何意义(对于我)。

无论如何,worker / wrapper转换并不是一个单构造函数的东西,构造函数的特化可以给几个构造函数的类型带来同样的好处。 (将创建多少个构造函数专门化取决于-fspec-constr-count的值。)


(1)这可能已经改变,但我对此表示怀疑。我没有检查过,所以页面可能已经过时了。

答案 1 :(得分:5)

我猜想GHC自2008年上次更新以来已经发生了很大变化。此外,您正在使用LLVM后端,因此这可能会对性能产生一些影响。 GHC可以(并且,因为您已使用-O2)从f中删除任何错误处理代码,因为它静态地知道f是完全的。 g不能说同样的话。我猜想这是LLVM后端,然后解压缩f中的构造函数标签,因为它很容易看到分支条件没有使用任何其他东西。不过,我不确定。