Подтвердить что ты не робот

Может ли GHC распаковывать перечисления на строгие поля данных?

В комментарии к предыдущему вопросу я утверждал:

У меня есть еще один тест, указывающий, что 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

Большое спасибо!

4b9b3361

Ответ 1

Первая

Даниэль Фишер предложил в своем ответе, что включение перечисления в строгом поле типа данных с несколькими конструкторами может помешать некоторым оптимизации.

вы это неправильно поняли. Если у вас есть конструктор C типа, не имеет значения, имеет ли этот тип несколько конструкторов или только один, со строгим полем типа T, C ... !T ..., тогда строковое поле можно распаковать, если T является оболочкой newtype вокруг неупакованного типа, но не если T является перечислением. В принципе, также возможно распаковать тег конструктора значения типа T, но GHC просто этого не делает (возможно, для этого причина может быть сложнее, чем я вижу). Тем не менее, при достаточно малом количестве перечислений указатель теги упомянутый Михаилом Гушенковым, должен иметь более или менее одинаковый эффект (не совсем, возможно).

Но для типов перечисления с более чем 3 или 7 (для 64-битных слов) конструкторов, где в некоторых случаях необходимо следовать указателю, разница должна проявляться.

Во-вторых, короткий ответ

На самом деле, мой реальный вопрос: стоит ли пытаться конвертировать перечисления в newtypes, когда имеет значение производительность?

Иногда.

Будет ли преобразование действительно повышать производительность и насколько оно зависит от того, что вы делаете со значениями. Это также может сделать вашу программу медленнее. И он может использовать больше памяти (см. Ниже).

Нет общего правила, каждый случай должен быть оценен. Существуют шаблоны, где newtype-wrapped Int работает быстрее, есть шаблоны, где они медленнее. Типичная программа будет содержать экземпляры обоих, и нужно выяснить, какие из них преобладают.


Теперь к эталону.

Я взял на себя смелость изменить аргументы в тесте, используя C вместо A и E 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 соответственно. E 1, разница была меньше, но в моем беге там тоже выиграл новый тип.

Обратите внимание, однако, что расчетная стоимость вызова clock примерно в четыре раза больше, чем среднее для любого из них, и расчетное тактовое разрешение более 100 раз. Я не уверен, что эти результаты надежны. Учитывая разницу, которую я наблюдаю в своей системе для краткосрочных тестов, я не доверяю бенчмаркам за все, что работает менее 10 микросекунд. Я предпочитаю, чтобы тесты работали дольше, потому что результаты были более стабильными и производилось меньше выбросов.

Относительно

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

К сожалению, iterate (force . succ') не форсирует элементы списка по мере его построения, поэтому вы получаете список thunks (с увеличением глубины), отбрасываете начальный сегмент этого и затем заставляете элементы списка.

Таким образом, подавляющая часть работы и распределения в этом случае заключается в построении thunks и списка, а затем в оценке thunks. Вы получаете более значимый результат, если вы предотвращаете создание больших thunks, заставляя элементы по мере их создания,

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

(шаблон взлома - WHNF - достаточно, чтобы полностью оценить значения рассматриваемых типов).

Тем не менее, существует наблюдаемая и последовательная разница в показателях распределения между перечислением и вариантами нового типа, и это также остается с iterate'. А также, если вместо того, чтобы отменить начальный сегмент и взять head, просто взять list !! index, где он становится еще более впечатляющим, потому что другие цифры (скопированные байты, максимальная резидентность) тогда малы.

Причина в том, что элементы списка не могут быть распакованы, поэтому ячейки списка содержат указатели на значения элементов, а значения перечисления являются общими (существует только одна A во всей программе), поэтому все эти указатели указывают на один и тот же объект, но целые числа не разделяются, поэтому каждая ячейка списка указывает на другой объект.

Разница в распределении в вашей системе составляет почти ровно 800 000 байт, на моих 1600 000.

Это то, что нужно 200 000 слов, для чего выделено 100 000 Word8 (или Word; Int,...) требуется (за каждое значение, одно слово для конструктора и одно для Word# или Int#).

Ответ 2

Не смотря на вывод компилятора, я думаю, что отсутствие увеличения скорости в новой версии вашего кода может быть связано с маркировкой указателя. На x86 GHC резервирует 2 бита в каждом указателе для получения информации о закрытии. 00 означает "неоценимый или неизвестный", а остальные 3 случая кодируют фактический тег оцененного конструктора. Эта информация обновляется динамически сборщиком мусора. Так как у вас всего три случая в типе тестовых данных, они всегда вписываются в биты тегов, и поэтому сопоставление шаблонов никогда не требует косвенности. Попробуйте добавить к вашему типу данных больше случаев и посмотрите, что произойдет. Более подробную информацию о размещении динамических указателей можно найти в этой статье:

Более быстрая лень с использованием тегов динамических указателей

Саймон Марлоу, Алексей Родригес Якушев и Саймон Пейтон Джонс, ICFP 2007.