前の質問へのコメントで、私は次のように主張しました。
ghc-7.4.1+llvm が厳密なデータ フィールドで列挙型のアンパックを行うことを示す別のベンチマークがあります。
実際、いくつかの実験の後、少なくともいくつかの単純化されたケースでは、列挙型を使用することは少なくとも同じくらい高速であり、実際には newtype の Word8 を使用するよりもメモリ効率が高い (したがって、より現実的なアプリケーションではより高速である) と私は信じています。 (または Int)、データ型の厳密なフィールドで使用された場合でも。前の質問で述べたように、私はより現実的な (しかしまだ小さい) 設定で同様の現象を経験しました。
ghc/llvm が列挙に対してどのような最適化を行うかについて、関連する参考文献を教えてもらえますか? 特に、厳密なデータ フィールドの列挙型の内部タグを実際に展開しますか? アセンブリの出力とプロファイリングの結果は、それが事実であることを示唆しているように見えますが、私にはコアレベルでの兆候はありません. どんな洞察も大歓迎です。
もう 1 つの質問: 列挙型は常に、対応する Integral の newtype と少なくとも同じくらい効率的ですか? (列挙も積分のように振る舞うことができることに注意してください。)そうでない場合、(うまくいけば現実的に役立つ)例外は何ですか?Daniel Fischer は、彼の回答で、複数コンストラクターのデータ型の厳密なフィールドに列挙を配置すると、特定の最適化が妨げられる可能性があると示唆しました。ただし、コンストラクターが 2 つの場合、これを検証できませんでした。それらを大きな複数コンストラクターのデータ型に入れると、違いがあるのではないでしょうか?
また、次のベンチマークで正確に何が起こっているのかにも興味があります。4 つのケースすべてで、ほぼ同じ量のバイトがヒープに割り当てられました。ただし、列挙の場合、GC が実際に行ったコピーは少なく、最大常駐数は newtype に比べて小さくなりました。
(実際、私の本当の質問は、パフォーマンスが重要な場合に、列挙型を newtypes に変換しようとする価値があるかどうかということでしたが、もう少し具体的にする方が役立つかもしれないと思いました。)
この質問の考えられる意味は次のとおりです。プログラムで多数の Int を使用していて、実際には非常に小さなサブセットで変化する場合、それらを (ボックス化されていない型ではなく) 列挙型に変更すると、パフォーマンスが低下する可能性があります。 gain (ただし、厳密には注意してください)。
以下は、ベンチマークの概要であり、その後にベンチマーク コードと、システムでテストするための便利な makefile が続きます。
ベンチマーク d 平均: 11.09113 ns、lb 11.06140 ns、ub 11.17545 ns、ci 0.950 標準偏差: 234.6722 ps、lb 72.31532 ps、ub 490.1156 ps、ci 0.950 ベンチマーク e 平均: 11.54242 ns、lb 11.51789 ns、ub 11.59720 ns、ci 0.950 標準偏差: 178.8556 ps、lb 73.05290 ps、ub 309.0252 ps、ci 0.950 ベンチマーク 平均: 11.74964 ns、lb 11.52543 ns、ub 12.50447 ns、ci 0.950 標準偏差: 1.803095 ns、lb 207.2720 ps、ub 4.029809 ns、ci 0.950 ベンチマーク t 平均: 11.89797 ns、lb 11.86266 ns、ub 11.99105 ns、ci 0.950 標準偏差: 269.5795 ps、lb 81.65093 ps、ub 533.8658 ps、ci 0.950 OK、列挙型は、少なくとも newtype よりも効率的であるように見えます 次に、関数のヒーププロファイル heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x データ D = A | ビ | 子: ヒープに割り当てられた 10,892,604 バイト GC 中にコピーされた 6,401,260 バイト 1,396,092 バイトの最大常駐 (3 サンプル) 55,940 バイトの最大スロップ 合計 6 MB の使用メモリ (断片化により 0 MB が失われる) 生産性 総ユーザーの 47.8%、総経過時間の 35.4% newtype E = E Word8: ヒープに割り当てられた 11,692,768 バイト GC 中に 8,909,632 バイトがコピーされました 2,779,776 バイトの最大常駐 (3 サンプル) 92,464 バイトの最大スロップ 合計 7 MB の使用メモリ (断片化により 0 MB が失われる) 生産性 総ユーザーの 36.9%、総経過時間の 33.8% データ S = S !D: ヒープに割り当てられた 10,892,736 バイト GC 中にコピーされた 6,401,260 バイト 1,396,092 バイトの最大常駐 (3 サンプル) 55,940 バイトの最大スロップ 合計 6 MB の使用メモリ (断片化により 0 MB が失われる) 生産性 総ユーザーの 48.7%、総経過時間の 33.3% データ T = T {-# UNPACK #-} !E: ヒープに割り当てられた 11,692,968 バイト GC 中に 8,909,640 バイトがコピーされました 2,779,760 バイトの最大常駐 (3 サンプル) 92,536 バイトの最大スロップ 合計 7 MB の使用メモリ (断片化により 0 MB が失われる) 生産性 総ユーザーの 36.1%、総経過時間の 31.6%
コンストラクターが 2 つの場合でも、同様のパフォーマンスの向上が得られます。
ベンチマーク コード (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.
ベンチマークに使用されるメイクファイル:
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
どうもありがとうございました!