Ефективність пам'яті Haskell - який кращий підхід?


11

Ми реалізуємо бібліотеку стиснення матриць на основі модифікованого синтаксису двомірної граматики. Зараз у нас є два підходи для наших типів даних - який буде кращим у випадку використання пам'яті? (ми хочемо щось стиснути;)).

Граматики містять NonTerminals з точно 4-ма творами або терміналом праворуч. Нам потрібні назви виробництва для перевірки рівності та мінімізації граматики.

Перший:

-- | Type synonym for non-terminal symbols
type NonTerminal = String

-- | Data type for the right hand side of a production
data RightHandSide = DownStep NonTerminal NonTerminal NonTerminal NonTerminal | Terminal Int

-- | Data type for a set of productions
type ProductionMap = Map NonTerminal RightHandSide

data MatrixGrammar = MatrixGrammar {
    -- the start symbol
    startSymbol :: NonTerminal,
    -- productions
    productions :: ProductionMap    
    } 

Тут наші дані RightHandSide зберігають лише імена рядків для визначення наступних постановок, і те, що ми тут не знаємо, - як Haskell зберігає ці рядки. Наприклад, матриця [[0, 0], [0, 0]] має 2 виробництва:

a = Terminal 0
aString = "A"
b = DownStep aString aString aString aString
bString = "B"
productions = Map.FromList [(aString, a), (bString, b)]

Тож питання тут полягає в тому, як часто рядок "A" дійсно зберігається? Один раз в aString, 4 рази в b і один раз в постановках або просто один раз в aString та інші просто містять «дешевші» посилання?

Другий:

data Production = NonTerminal String Production Production Production Production
                | Terminal String Int 

type ProductionMap = Map String Production

тут термін "Термінал" трохи вводить в оману, оскільки його фактично є виробництво, яке має термінал як праву сторону. Та сама матриця:

a = Terminal "A" 0
b = NonTerminal "B" a a a a
productions = Map.fromList [("A", a), ("B", b)]

і подібне питання: як часто продукція Haskell внутрішньо економиться? Можливо, ми викинемо імена всередині постановок, якщо вони нам не знадобляться, але ми зараз не впевнені в цьому.

Тож скажімо, у нас є граматика з близько 1000 творів. Який підхід буде споживати менше пам’яті?

Нарешті, питання про цілі числа в Haskell: В даний час ми плануємо назвати ім'я як Strings. Але ми могли б легко перейти до цілих імен, тому що з 1000 виробництв у нас з'являться імена з більш ніж 4 символами (що я вважаю, це 32 біт?). Як справляється з цим Haskell. Чи Int завжди 32-бітний і цілий виділяє пам'ять, яка йому справді потрібна?

Я також прочитав це: Розробка тесту на значення / еталонну семантику Haskell - але я не можу зрозуміти, що саме це для нас означає - я більше імперативний дитина Java, а потім хороший функціональний програміст: P

Відповіді:


7

Ви можете розширити свою матричну граматику в ADT за допомогою ідеального спільного використання з невеликою хитрістю:

{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}

import Data.Map
import Data.Foldable
import Data.Functor
import Data.Traversable

-- | Type synonym for non-terminal symbols
type NonTerminal = String

-- | Data type for the right hand side of a production
data RHS a = DownStep NonTerminal NonTerminal NonTerminal NonTerminal | Terminal a
  deriving (Eq,Ord,Show,Read,Functor, Foldable, Traversable)

data G a = G NonTerminal (Map NonTerminal (RHS a))
  deriving (Eq,Ord,Show,Read,Functor)

data M a = Q (M a) (M a) (M a) (M a) | T a
  deriving (Functor, Foldable, Traversable)

tabulate :: G a -> M a
tabulate (G s pm) = loeb (expand <$> pm) ! s where
  expand (DownStep a11 a12 a21 a22) m = Q (m!a11) (m!a12) (m!a21) (m!a22)
  expand (Terminal a)               _ = T a

loeb :: Functor f => f (f b -> b) -> f b
loeb x = xs where xs = fmap ($xs) x

Тут я узагальнив ваші граматики, щоб дозволити будь-який тип даних, а не лише Int, і tabulateвізьме граматику та розширить її, склавши її на себе за допомогою loeb.

loebописано у статті Дана Піпоні

Отримане розширення як ADT фізично займає не більше пам’яті, ніж оригінальна граматика - насправді це займає трохи менше, тому що йому не потрібен додатковий коефіцієнт журналу для хребта Map, і його не потрібно зберігати струни взагалі.

На відміну від наївного розширення, використання loebдозволяє «зв’язати вузол» і поділитися гронами на всі випадки того самого нетермінального.

Якщо ви хочете більше зануритися в теорію всього цього, ми можемо побачити, що RHSможна перетворити на базовий функтор:

data RHS t nt = Q nt nt nt nt | L t

і тоді мій тип М - це лише фіксована точка цього Functor.

M a ~ Mu (RHS a)

при цьому G aскладається з обраного рядка та карти від рядків до (RHS String a).

Потім ми можемо розширити Gв Mпо LookUp до запису в карті розширених рядків Ліниво.

Це щось на зразок дуалу того, що робиться в data-reifyпакеті, який може взяти такий базовий функтор, і щось подібне Mі відновити з нього моральний еквівалент G. Вони використовують інший тип для нетермінальних імен, що в основному є лише an Int.

data Graph e = Graph [(Unique, e Unique)] Unique

і забезпечити комбінатор

reifyGraph :: MuRef s => s -> IO (Graph (DeRef s))

який може бути використаний з відповідним екземпляром для вищезазначених типів даних для отримання графіка (MatrixGrammar) з довільної матриці. Він не буде виконувати дедупликацію однакових, але окремо збережених квадрантів, але відновить весь спільний доступ, який присутній у вихідному графіку.


8

У Haskell тип String - псевдонім для [Char], що є звичайним списком Хаскелл Char, а не вектором чи масивом. Char - тип, який містить один символ Unicode. Якщо ви не використовуєте розширення мови, рядкові літерали - це значення типу String.

Я думаю, що ви можете здогадатися з вищесказаного, що String - це не дуже компактне або інакше ефективне представлення. Загальні альтернативні подання для рядків включають типи, надані Data.Text та Data.ByteString.

Для додаткової зручності ви можете використовувати -XOverloadedStrings, щоб ви могли використовувати рядкові літерали як представлення альтернативного типу рядка, такого як надано Data.ByteString.Char8. Це, мабуть, найефективніший спосіб зручного використання рядків як ідентифікаторів.

Що стосується Int, це тип фіксованої ширини, але немає гарантії того, наскільки він широкий, за винятком того, що він повинен бути досить широким, щоб утримувати значення [-2 ^ 29 .. 2 ^ 29-1]. Це дозволяє припустити, що принаймні 32 біти, але не виключає, що це 64 біти. Data.Int має деякі більш конкретні типи, Int8-Int64, які ви можете використовувати, якщо вам потрібна конкретна ширина.

Редагувати, щоб додати інформацію

Я не вірю, що семантика Haskell так чи інакше визначає обмін даними. Не слід очікувати, що два рядкових літерали або два з будь-яких побудованих даних посилаються на один і той же "канонічний" об'єкт у пам'яті. Якби ви прив’язували побудоване значення до нового імені (з пусканням, збігом шаблонів тощо), обидва імена, швидше за все, посилалися б на одні і ті ж дані, але те, чи вони так чи ні, насправді не видно через незмінний характер Дані Haskell

Задля ефективності зберігання ви можете інтернувати рядки, які, по суті, зберігають канонічне зображення кожного з таблиць пошуку певного роду, як правило, хеш-таблиці. Коли ви інтернуєте об'єкт, ви отримуєте дескриптор для нього назад, і ви можете порівнювати ці дескриптори з іншими, щоб побачити, чи вони такі ж набагато дешевше, ніж ви могли б струнних, і вони також часто набагато менші.

Для бібліотеки, яка займається стажуванням, ви можете використовувати https://github.com/ekmett/intern/

Щодо вирішення, який цілий розмір використовувати під час виконання, досить просто написати код, який залежить від класів типу Integral або Num замість конкретних числових типів. Висновок типу дасть вам найбільш загальні типи, які він може автоматично. Потім у вас може бути кілька різних функцій із типами, явно звуженими до конкретних числових типів, які ви можете вибрати один із під час виконання для первинної настройки, і після цього всі інші поліморфні функції будуть працювати однаково на будь-якому з них. Наприклад:

polyConstructor :: Integral a => a -> MyType a
int16Constructor :: Int16 -> MyType Int16
int32Constructor :: Int32 -> MyType Int32

int16Constructor = polyConstructor
int32Constructor = polyConstructor

Редагувати : Більше інформації про стажування

Якщо ви хочете лише інтернувати рядки, ви можете створити новий тип, який обертає рядок (бажано, Text або ByteString) та невелике ціле число разом.

data InternedString = { id :: Int32, str :: Text }
instance Eq InternedString where
    {x, _ } == {y, _ }  =  x == y

intern :: MonadIO m => Text -> m InternedString

Що "intern" робить, це шукати рядок у слабкому посиланні HashMap, де тексти є ключами, а InternedStrings - значеннями. Якщо відповідність знайдена, 'intern' повертає значення. Якщо ні, то створюється нове значення InternedString з оригінальним текстом та унікальним цілим ідентифікатором (саме тому я включив обмеження MonadIO; він міг би використовувати монаду State або якусь небезпечну операцію замість цього, щоб отримати унікальний ідентифікатор; є багато можливостей) і зберігає його на карті перед поверненням.

Тепер ви отримуєте швидке порівняння на основі цілого ідентифікатора і маєте лише одну копію кожного унікального рядка.

Бібліотека стажерів Едварда Кметта застосовує той самий принцип, більш-менш, набагато більш загальний спосіб, щоб цілі структуровані терміни даних хеширувались, зберігалися унікально та отримували операцію швидкого порівняння. Це трохи непросто і не особливо задокументовано, але він може бути готовий допомогти, якщо ви попросите; або ви можете спробувати спочатку власну реалізацію струнного інтернування, щоб побачити, чи достатньо вона допомагає.


Дякую за відповідь поки що. Чи можна визначити, який розмір int ми повинні використовувати під час виконання? Я сподіваюся, що хтось інший може дати деякий внесок у проблему з копіями :)
Dennis Ich

Дякуємо за додану інформацію. Я погляну туди. Просто, щоб правильно сказати, це дескриптори, з якими ви говорите, є чимось на зразок посилання, яке хеширується і яке порівняння? Ви працювали з цим своїм «я»? Чи можете ви сказати, наскільки це "складніше" з цим, тому що на перший погляд здається, що я повинен бути дуже обережним, а потім із визначенням граматик;)
Dennis Ich

1
Автор цієї бібліотеки - дуже просунутий користувач Haskell, відомий якісною роботою, але я не використовував цю конкретну бібліотеку. Це дуже "загальна хеш" реалізація загального призначення, яка зберігатиме та надаватиме можливість спільного використання представлень у будь-якому побудованому типі даних, а не лише у рядках. Подивіться у його прикладі з прикладом проблеми, подібної вашій, і ви можете побачити, як реалізуються функції рівності.
Леві Пірсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.