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

Являются ли новые типы быстрее, чем перечисления?

Согласно этой статье,

Перечисления не учитываются как типы одного конструктора в отношении GHC, поэтому они не извлекаются из распаковки при использовании в качестве строгих полей конструктора или строгих аргументов функции. Это недостаток в GHC, но его можно обойти.

И вместо этого рекомендуется использовать новые типы. Однако я не могу проверить это с помощью следующего кода:

{-# 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 не имеет кода обработки ошибок и более чем на 10% быстрее, чем g. В более реалистичном случае я также испытал замедление после преобразования перечисления в новый тип. Может ли кто-нибудь дать мне некоторое представление об этом? Спасибо.

4b9b3361

Ответ 1

Это зависит от варианта использования. Для функций, которые у вас есть, ожидалось, что перечисление будет работать лучше. В принципе, три конструктора D становятся Int соответственно. Int#, когда анализ строгости позволяет это, и GHC знает, что он статически проверяет, что аргумент может иметь только одно из трех значений 0#, 1#, 2#, поэтому ему не нужно вставлять код обработки ошибок для f. Для E статическая гарантия только одного из трех возможных значений не задана, поэтому ей необходимо добавить код обработки ошибок для g, что значительно замедляет работу. Если вы измените определение g так, чтобы последний случай стал

E _ -> 3535#

разница полностью или почти полностью исчезает (я получаю 1% - 2% лучший тест для f, но я не сделал достаточного тестирования, чтобы убедиться, что это реальная разница или артефакт бенчмаркинга).

Но это не тот случай, о котором говорит страница wiki. Речь идет о распаковке конструкторов в другие конструкторы, когда тип является компонентом других данных, например

data FooD = FD !D !D !D

data FooE = FE !E !E !E

Затем, если скомпилировано с -funbox-strict-fields, три Int# могут быть распакованы в конструктор FooE, поэтому вы в основном получите эквивалент

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

в то время как поля FooD имеют тип multi-constructor D и не могут быть распакованы в конструктор FD (1) так что это в основном даст вам

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

Это, очевидно, может оказать значительное влияние.

Я не уверен в случае аргументов функции с одним конструктором. Это имеет очевидные преимущества для типов с содержащимися данными, такими как кортежи, но я не вижу, как это применимо к простым перечислениям, где у вас просто есть case и разделение рабочего и обертка не имеет смысла (для меня).

Во всяком случае, преобразование рабочего/обертки не является чем-то вроде одного конструктора, специализация конструктора может принести одинаковое преимущество типам с несколькими конструкторами. (Для того, сколько конструкторов будет создано, зависит от значения -fspec-constr-count.)


(1) Это могло измениться, но я сомневаюсь. Я не проверял его, так что, возможно, страница устарела.

Ответ 2

Я бы предположил, что GHC изменился совсем немного, так как эта страница была в последний раз обновлена ​​в 2008 году. Кроме того, вы используете бэкэнд LLVM, так что, вероятно, также окажете некоторое влияние на производительность. GHC может (и будет, с тех пор, как вы использовали -O2), удалить любой код обработки ошибок из f, потому что он знает статически, что f является итоговым. То же самое нельзя сказать о g. Я бы предположил, что это бэкенд LLVM, который затем распаковывает теги конструктора в f, потому что он легко может видеть, что условие ветвления больше ничего не используется. Я не уверен в этом.