GHC可以在严格的数据字段上枚举枚举吗?

在对前一个问题的评论中,我声称:

我有另一个基准来表明ghc-7.4.1 + llvm会在严格的数据字段上对枚举进行解包。

事实上,经过一些实验后,我相信在至少一些简单的情况下,使用枚举至少与使用新类型的Word8一样快,并且实际上可能会更有效率(因此在更现实的应用中速度更快) (或Int),即使在数据类型的严格字段中使用时也是如此。 正如我在前一个问题中所说的,我在一个更现实的(但仍然很小)的环境中经历了类似的现象。

任何人都可以向我指出一些相关的参考资料,说明ghc / llvm对于枚举有什么优化? 特别是,它是否真的在枚举的内部标签上打包了严格的数据字段? 程序集的输出和分析的结果似乎表明,情况就是这样,但对我而言,核心级别没有迹象。 任何见解将不胜感激。

还有一个问题:枚举总是至少和相应的Integral的新类型一样有效,在这里使用它们是有意义的? (请注意枚举也可以像积分一样。)如果不是,那么(希望实际上有用的)异常是什么? Daniel Fischer在他的回答中建议,将枚举放在多构造函数数据类型的严格字段上可能会阻止某些优化。 但是我没有在双构造函数中验证这一点。 将它们放入大型多重构造函数数据类型中时可能有区别吗?

我也很好奇下面的基准测试究竟发生了什么。 在所有这四种情况下,在堆中分配的字节大致相同。 但是,对于枚举,GC实际上复制的次数较少,与newtypes相比,最大驻留次数更少。

(其实我真正的问题是,在性能问题时尝试将枚举转换为新类型是值得的,但我认为更具体一点可能会更有帮助。)

这个问题的一个可能的含义是:如果你的程序中使用了大量的Int,它们在一些非常小的子集上实际上有所不同,那么将它们改为枚举(而不是非盒装类型!)可能是性能获得(但谨慎严格)。

以下是基准的总结,其次是基准代码和用于在系统上进行测试的便捷makefile。

benchmarking d
mean: 11.09113 ns, lb 11.06140 ns, ub 11.17545 ns, ci 0.950
std dev: 234.6722 ps, lb 72.31532 ps, ub 490.1156 ps, ci 0.950

benchmarking e
mean: 11.54242 ns, lb 11.51789 ns, ub 11.59720 ns, ci 0.950
std dev: 178.8556 ps, lb 73.05290 ps, ub 309.0252 ps, ci 0.950

benchmarking s
mean: 11.74964 ns, lb 11.52543 ns, ub 12.50447 ns, ci 0.950
std dev: 1.803095 ns, lb 207.2720 ps, ub 4.029809 ns, ci 0.950

benchmarking t
mean: 11.89797 ns, lb 11.86266 ns, ub 11.99105 ns, ci 0.950
std dev: 269.5795 ps, lb 81.65093 ps, ub 533.8658 ps, ci 0.950

OK,so the enumeration appears at least no less efficient than the newtype
Next,heap profiles of the function
heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x

data    D = A | B | C:
      10,892,604 bytes allocated in the heap
       6,401,260 bytes copied during GC
       1,396,092 bytes maximum residency (3 sample(s))
          55,940 bytes maximum slop
               6 MB total memory in use (0 MB lost due to fragmentation)
  Productivity  47.8% of total user, 35.4% of total elapsed

newtype E = E Word8:
      11,692,768 bytes allocated in the heap
       8,909,632 bytes copied during GC
       2,779,776 bytes maximum residency (3 sample(s))
          92,464 bytes maximum slop
               7 MB total memory in use (0 MB lost due to fragmentation)
  Productivity  36.9% of total user, 33.8% of total elapsed

data  S = S !D:
      10,892,736 bytes allocated in the heap
       6,401,260 bytes copied during GC
       1,396,092 bytes maximum residency (3 sample(s))
          55,940 bytes maximum slop
               6 MB total memory in use (0 MB lost due to fragmentation)
  Productivity  48.7% of total user, 33.3% of total elapsed

data  T = T {-# UNPACK #-} !E:
      11,692,968 bytes allocated in the heap
       8,909,640 bytes copied during GC
       2,779,760 bytes maximum residency (3 sample(s))
          92,536 bytes maximum slop
               7 MB total memory in use (0 MB lost due to fragmentation)
  Productivity  36.1% of total user, 31.6% of total elapsed

在构造函数的情况下可以获得类似的性能增益。

基准代码(保存为EnumTest.hs):


{-# LANGUAGE CPP,MagicHash , BangPatterns ,GeneralizedNewtypeDeriving #-}
module Main(main,d,e,s,t,D(..),E(..),S(..),T(..))
where       
import GHC.Base  
import Data.List
import Data.Word
import Control.DeepSeq
import Criterion.Main

data    D = A | B | C  deriving(Eq,Ord,Show,Enum,Bounded)
newtype E = E Word8    deriving(Eq,Ord,Show,Enum)

data    S = S                !D deriving (Eq,Ord,Show) 
data    T = T {-# UNPACK #-} !E deriving (Eq,Ord,Show)

-- I assume the following definitions are all correct --- otherwise
-- the whole benchmark may be useless
instance NFData D where
  rnf !x         = ()
instance NFData E where
  rnf (E !x)     = ()
instance NFData S where
  rnf (S !x)     = ()
instance NFData T where
  rnf (T (E !x)) = ()  

instance Enum S where
  toEnum         = S . toEnum
  fromEnum (S x) = fromEnum x 
instance Enum T where
  toEnum         = T . toEnum
  fromEnum (T x) = fromEnum x 

instance Bounded E where
  minBound = E 0
  maxBound = E 2
instance Bounded S where
  minBound = S minBound
  maxBound = S maxBound
instance Bounded T where
  minBound = T minBound
  maxBound = T maxBound

succ' :: (Eq a,Enum a,Bounded a) => a -> a
succ' x | x == maxBound = minBound
          | otherwise        = succ x

-- Those numbers below are for easy browsing of the assembly code
d :: D -> Int#
d x = case x of
  A -> 1234#
  B -> 5678#
  C -> 9412#

e :: E -> Int#
e x = case x of     
  E 0 -> 1357#
  E 1 -> 2468#
  E _ -> 9914#

s :: S -> Int#
s x = case x of     
  S A -> 9876#
  S B -> 5432#
  S C -> 1097#

t :: T -> Int#
t x = case x of     
  T (E 0) -> 9630#
  T (E 1) -> 8529#
  T (E _) -> 7418#


benchmark :: IO ()
benchmark = defaultMain [ bench "d" $ whnf d' A
                        , bench "e" $ whnf e' (E 0)
                        , bench "s" $ whnf s' (S A) 
                        , bench "t" $ whnf t' (T (E 0))
                        ]
  where
    d' x = I# (d x)
    e' x = I# (e x)
    s' x = I# (s x)
    t' x = I# (t x)    

heapTest :: (NFData a,Show a,Eq a,Enum a,Bounded a) => a -> IO ()
heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x

main :: IO ()
main =    
#if   defined TEST_D
     heapTest (A :: D)
#elif defined TEST_E                
     heapTest (E 0 :: E)
#elif defined TEST_S
     heapTest (S A :: S)
#elif defined TEST_T                
     heapTest (T (E 0) :: T)
#else
     benchmark
#endif

-- A minor rant: 
-- For reliable statistics, I hope Criterion will run the code in *random order*,
-- at least for comparing functions with the same type. Elapsed times on my system are just too
-- noisy to conclude anything.

用于基准测试的makefile:


GHC=/usr/bin/ghc
# If you dont't like the ATT syntax in the output assembly, use this: -fllvm -optlc --x86-asm-syntax=intel
GHC_DEBUG_FLAGS= -keep-s-file -keep-llvm-file  # -optlc --x86-asm-syntax=intel
GHCFLAGS=-O2 -funbox-strict-fields -rtsopts -fllvm -fwarn-missing-signatures
GHC_MAKE=$(GHC) --make $(GHCFLAGS)
GHC_PROF_MAKE=$(GHC)  -prof  -auto-all -caf-all --make $(GHCFLAGS)

all : benchmark enumtest_all

enumtest_d : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_D

enumtest_e : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_E

enumtest_s : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_S

enumtest_t : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_T

enumtest_all : enumtest_d enumtest_e enumtest_s enumtest_t
    for x in $^; do ./$$x +RTS -sstderr ;done

benchmark : EnumTest
    time ./$^

% : %.hs
    $(GHC_MAKE) -o $@ $^

%.core : %.hs
    $(GHC)  -S  $(GHCFLAGS)   $(GHC_DEBUG_FLAGS) -ddump-simpl -dsuppress-all -dsuppress-coercions -ddump-stranal $^ > $@

clean :
    rm *.hi *.o *.core *.s enumtest_? ; true

非常感谢你!


第一

Daniel Fischer在他的回答中建议,将枚举放在多构造函数数据类型的严格字段上可能会阻止某些优化。

你误解了这一点。 如果你有一个类型的构造函数C ,那么这个类型是否有多个构造函数或者只有一个,并且有严格的类型TC ... !T ... ,那么strict字段可以解压缩T是一个不可压缩类型的新类型包装器,但如果T是一个枚举类型,则不是。 原则上可以解压类型T的构造函数标签,但是GHC不会这么做(这可能是有原因的,它可能比我看到的更复杂)。 尽管如此,对于足够小的枚举类型来说,Mikhail Gushenkov提到的指针标签应该或多或少具有相同的效果(可能并不完全)。

但对于枚举类型超过3或7(对于64位字)的构造函数,在某些情况下必须跟随指针,这种差异应该会显现出来。

其次,简要回答

其实我真正的问题是,当表现很重要时,是否值得尝试将枚举转换为新类型?

有时。

转换是否实际上可以提高性能以及提高多少取决于您对值所做的操作。 它也可能会让你的程序变慢。 它可能会使用更多的内存(见下文)。

没有一般规则,每个案例都需要评估。 有新模式包装的Int更快,模式更慢。 一个典型的程序将包含两者的实例,并且必须找出哪个占优势。


现在到基准。

我冒昧地改变了基准中的论点,用C代替AE 2代替E 0 。 结果是这些新类型的速度略快:

warming up
estimating clock resolution...
mean is 1.549612 us (640001 iterations)
found 4506 outliers among 639999 samples (0.7%)
  3639 (0.6%) high severe
estimating cost of a clock call...
mean is 39.24624 ns (12 iterations)
found 2 outliers among 12 samples (16.7%)
  1 (8.3%) low mild
  1 (8.3%) high severe

benchmarking d
mean: 12.12989 ns, lb 12.01136 ns, ub 12.32002 ns, ci 0.950
std dev: 755.9999 ps, lb 529.5348 ps, ub 1.034185 ns, ci 0.950
found 17 outliers among 100 samples (17.0%)
  17 (17.0%) high severe
variance introduced by outliers: 59.503%
variance is severely inflated by outliers

benchmarking e
mean: 10.82692 ns, lb 10.73286 ns, ub 10.98045 ns, ci 0.950
std dev: 604.1786 ps, lb 416.5018 ps, ub 871.0923 ps, ci 0.950
found 10 outliers among 100 samples (10.0%)
  4 (4.0%) high mild
  6 (6.0%) high severe
variance introduced by outliers: 53.482%
variance is severely inflated by outliers

benchmarking s
mean: 13.18192 ns, lb 13.11898 ns, ub 13.25911 ns, ci 0.950
std dev: 354.1332 ps, lb 300.2860 ps, ub 406.2424 ps, ci 0.950
found 13 outliers among 100 samples (13.0%)
  13 (13.0%) high mild
variance introduced by outliers: 20.952%
variance is moderately inflated by outliers

benchmarking t
mean: 11.16461 ns, lb 11.02716 ns, ub 11.37018 ns, ci 0.950
std dev: 853.2152 ps, lb 602.5197 ps, ub 1.086899 ns, ci 0.950
found 14 outliers among 100 samples (14.0%)
  3 (3.0%) high mild
  11 (11.0%) high severe
variance introduced by outliers: 68.689%
variance is severely inflated by outliers

因此,在任何一种情况下,基准都没有显着差异,总体结果取决于所提供的论点。 随着B resp。 E 1 ,差距较小,但在我看来,新类型也在那里获胜。

但请注意, clock通话的估计成本大约是其中任何一个的平均值的四倍,并且估计的时钟分辨率超过了100倍。 我不相信这些结果是可靠的。 考虑到我在我的系统上观察到的短基准差异,我不相信任何运行时间小于10微秒的基准测试。 我更喜欢测试运行时间更长,因为结果更稳定并且生成的离群值更少。

关于

heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x

不幸的是, iterate (force . succ')不会强制列表中的元素,因此您会得到一个thunk列表(增加深度),反转其初始段,然后强制列表元素。

因此,所做的工作和分配的绝大部分是建立连接和列表,然后评估连接。 如果您通过强制生成元素来阻止构建大型连线,则会得到更有意义的结果,

iterate' :: (a -> a) -> a -> [a]
iterate' f !a = a : iterate' f (f a)

(爆炸模式 - WHNF - 足以完全评估所讨论类型的值)。

尽管如此,枚举和新类型变量之间的分配数字仍然存在明显的一致性差异,并且仍然存在iterate' 。 而且,如果不是颠倒一个初始阶段并取得这个阶段的head ,而是简单地list !! index list !! index ,其中它变得更加令人印象深刻,因为其他数字(复制的字节数,最大驻留)则很小。

原因是列表元素不能被拆箱,因此列表单元格包含指向元素值的指针,并且枚举值是共享的(在整个程序中只存在一个A ),所以所有这些指针都指向同一个对象,但整数不共享,所以每个列表单元格指向一个不同的对象。

分配的差异在您的系统上差不多800,000字节,在我的1,600,000字节上。

这就是200,000个单词所需要的,100,000个Word8 (或Word ; Int ,...)需要的分配量(每个值,一个单词用于构造函数,另一个用于Word#Int# )。


不考虑编译器输出,我认为在代码的新版本中没有提高速度可能是由于指针标记。 在x86上,GHC在每个指针中保留2位以获取有关指向闭包的信息。 00表示“未评估或未知”,另外三种情况编码评估构造函数的实际标记。 该信息由垃圾收集器动态更新。 由于您的测试数据类型只有3种情况,因此它们始终适合标记位,因此模式匹配从不需要间接引导。 尝试向您的数据类型添加更多案例,并查看会发生什么。 您可以在本文中找到有关动态指针标记的更多信息:

使用动态指针标记更快地懒惰

ICFP 2007年的Simon Marlow,Alexey Rodriguez Yakushev和Simon Peyton Jones。

链接地址: http://www.djcxy.com/p/43215.html

上一篇: Can GHC unpack enumerations on strict data fields?

下一篇: Profiling GHC generated functions with `deriving` keyword