В комментарии к предыдущему вопросу я утверждал:
У меня есть еще один тест, указывающий, что ghc-7.4.1 + llvm делает распаковку перечислений в строгих полях данных.
Фактически, после некоторых экспериментов я считаю, что, по крайней мере, в некоторых упрощенных случаях, использование перечисления, по крайней мере, так же быстро, как и фактически может быть более эффективным с точки зрения памяти (следовательно, быстрее в более реалистичном приложении), чем использование нового типа Word8 (или Int), даже если оно используется в строгом поле типа данных. Как я сказал в предыдущем вопросе, я испытал подобное явление в более реалистичной (но все же маленькой) обстановке.
Может ли кто-нибудь указать мне на некоторые релевантные ссылки относительно того, какие оптимизации ghc/llvm делают для перечислений? В частности, действительно ли он распаковывает внутренний тег перечисления в строгом поле данных? Вывод сборки и результат профилирования, по-видимому, показывают, что это так, но для меня нет индикатора на основном уровне. Любые идеи будут с благодарностью оценены.
Еще один вопрос: Являются ли перечисления всегда не менее эффективными, чем новые типы соответствующего Интеграла, где их использование имеет смысл? (Обратите внимание, что перечисления могут вести себя как Интегралы тоже.) Если нет, то каково (надеюсь, реально полезное) исключение? Даниэль Фишер предложил в свой ответ, что включение перечисления в строгом поле типа данных с несколькими конструкторами может помешать некоторым оптимизации. Однако я не смог проверить это в случае с двумя конструкторами. Может быть, есть разница при помещении их в большие типы данных с несколькими конструкторами?
Мне также интересно, что именно происходит в следующем эталоне. Во всех четырех случаях в куче было выделено примерно такое же количество байтов. Однако для перечислений GC фактически уменьшал количество копий, а максимальные остатки были меньше по сравнению с новыми типами.
(На самом деле, мой реальный вопрос: стоит ли пытаться конвертировать перечисления в newtypes, когда производительность имеет значение?, но я предположил, что быть более конкретным может быть более полезным.)
Возможным следствием этого вопроса будет следующее: если вы используете большое количество Int в вашей программе, которое действительно меняется на очень маленьком подмножестве, то изменение их на перечисление (а не на unboxed type!) может быть приростом производительности (но осторожно для строгости).
Вот краткое изложение теста, за которым следует тестовый код и удобный 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 [email protected] $^ -DTEST_D
enumtest_e : EnumTest.hs
$(GHC_MAKE) -o [email protected] $^ -DTEST_E
enumtest_s : EnumTest.hs
$(GHC_MAKE) -o [email protected] $^ -DTEST_S
enumtest_t : EnumTest.hs
$(GHC_MAKE) -o [email protected] $^ -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 [email protected] $^
%.core : %.hs
$(GHC) -S $(GHCFLAGS) $(GHC_DEBUG_FLAGS) -ddump-simpl -dsuppress-all -dsuppress-coercions -ddump-stranal $^ > [email protected]
clean :
rm *.hi *.o *.core *.s enumtest_? ; true
Большое спасибо!