Как узнать представления GHC-памяти типов данных?

В последнее время записи в блоге, такие как "Вычисление размера хэш-карты" , объясняют, как рассуждать о пространственных сложностях обычно используемых типов контейнеров. Теперь я столкнулся с вопросом, как на самом деле "видеть", какую макет памяти выбирает моя версия GHC (в зависимости от флагов компиляции и целевой архитектуры) для странных типов данных (конструкторов), таких как

data BitVec257 = BitVec257 {-# UNPACK #-} !Word64
                           {-# UNPACK #-} !Word64
                           {-# UNPACK #-} !Bool
                           {-# UNPACK #-} !Word64
                           {-# UNPACK #-} !Word64

data BitVec514 = BitVec514 {-# UNPACK #-} !BitVec257
                           {-# UNPACK #-} !BitVec257

В C есть оператор sizeof и offsetof, который позволяет мне "видеть", какой размер и выравнивание были выбраны для полей C struct.

Я пытался посмотреть на GHC Core в надежде найти какой-то намек, но я не знал, что искать. Может ли кто-нибудь указать мне в правильном направлении?

Ответ 1

Моя первая идея состояла в том, чтобы использовать эту опрятную функцию litte из-за Саймона Марлоу:

{-# LANGUAGE MagicHash,UnboxedTuples #-}
module Size where

import GHC.Exts
import Foreign

unsafeSizeof :: a -> Int
unsafeSizeof a =
  case unpackClosure# a of
    (# x, ptrs, nptrs #) ->
      sizeOf (undefined::Int) + -- one word for the header
        I# (sizeofByteArray# (unsafeCoerce# ptrs)
             +# sizeofByteArray# nptrs)

Используя его:

Prelude> :!ghc -c Size.hs

Size.hs:15:18:
    Warning: Ignoring unusable UNPACK pragma on the
             third argument of `BitVec257'
    In the definition of data constructor `BitVec257'
    In the data type declaration for `BitVec257'
Prelude Size> unsafeSizeof $! BitVec514 (BitVec257 1 2 True 3 4) (BitVec257 1 2 True 3 4)
74

(Обратите внимание, что GHC сообщает вам, что он не может распаковать Bool, поскольку это тип суммы.)

Вышеупомянутая функция утверждает, что ваш тип данных использует 74 байта на 64-битной машине. Я считаю, что трудно поверить. Я бы ожидал, что тип данных будет использовать 11 слов = 88 байт, по одному слову на поле. Даже Bool принимают одно слово, поскольку они являются указателями на (статически выделенные) конструкторы. Я не совсем уверен, что происходит здесь.

Что касается выравнивания, я считаю, что каждое поле должно быть выровнено по словам.

Ответ 2

Отпечатки памяти типов данных Haskell

(Следующее относится к GHC, другие компиляторы могут использовать разные соглашения о хранении)

Правило большого пальца: конструктор стоит одно слово для заголовка и одно слово для каждого поля. Исключение: конструктор без полей (например, Nothing или True) не занимает места, потому что GHC создает один экземпляр этих конструкторов и разделяет его среди всех применений.

Слово представляет собой 4 байта на 32-битной машине и 8 байтов на 64-разрядной машине.

Так, например,

data Uno = Uno a
data Due = Due a b

a Uno принимает 2 слова, а функция Due принимает 3.

Также я считаю, что можно написать функцию haskell, которая выполняет те же задачи, что и sizeof или offsetof