Haskell bellek verimliliği - hangisi daha iyi bir yaklaşımdır?


11

Değiştirilmiş iki boyutlu dilbilgisi sözdizimine dayalı bir matris sıkıştırma kütüphanesi uyguluyoruz. Şimdi veri tiplerimiz için iki yaklaşımımız var - bellek kullanımı durumunda hangisi daha iyi olacak? (bir şeyi sıkıştırmak istiyoruz;)).

Dilbilgisi, tam olarak 4 Prodüksiyona sahip olmayan Terminaller veya sağ tarafta bir Terminal içerir. Eşitlik kontrolleri ve dilbilgisi minimizasyonu için Productions isimlerine ihtiyacımız olacak.

İlk:

-- | 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    
    } 

Burada RightHandSide verilerimiz, bir sonraki yapımları belirlemek için yalnızca String adlarını kaydeder ve burada bilmediğimiz şey Haskell'in bu karakter dizilerini nasıl kaydettiğidir. Örneğin [[0, 0], [0, 0]] matrisinin 2 prodüksiyonu vardır:

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

Buradaki soru, "A" Stringinin ne sıklıkta kaydedildiğidir? Bir kez aString, 4 kez b ve bir kez yapımlarda veya sadece bir kez aString ve diğerleri sadece "daha ucuz" referanslar tutuyor?

İkinci:

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

type ProductionMap = Map String Production

burada "Terminal" terimi biraz yanıltıcıdır, çünkü aslında sağ taraftaki bir terminale sahip olan üretimdir. Aynı Matris:

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

ve benzer soru: üretim Haskell tarafından dahili olarak ne kadar tasarruf ediliyor? Muhtemelen onlara ihtiyacımız yoksa isimleri yapımların içine bırakacağız, ancak şu anda bu konuda emin değiliz.

Diyelim ki yaklaşık 1000 prodüksiyona sahip bir gramerimiz var. Hangi yaklaşım daha az bellek tüketir?

Son olarak Haskell'deki tamsayılarla ilgili bir soru: Şu anda Dizeler olarak isim almayı planlıyoruz. Ama kolayca tamsayı isimlerine geçebiliriz, çünkü 1000 yapımında 4 karakterden daha uzun isimlere sahip olacağız (ki 32 bit olduğunu varsayıyorum). Haskell bunu nasıl halleder. Int her zaman 32 Bit ve Integer gerçekten ihtiyaç duyduğu belleği ayırır mı?

Bunu da okudum: Haskell'in değer / referans anlambilim testinin geliştirilmesi - ama bunun bizim için tam olarak ne anlama geldiğini anlayamıyorum - Ben daha çok zorunlu bir java çocuğuyum, sonra iyi bir işlevsel programcı: P

Yanıtlar:


7

Matris dilbilginizi, biraz hile ile mükemmel paylaşımla bir ADT'ye genişletebilirsiniz :

{-# 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

Burada sadece Int değil, herhangi bir veri türüne izin vermek için dilbilgilerinizi genelleştirdim ve dilbilgisini tabulatealıp üzerine katlayarak genişleteceğiz loeb.

loebanlatılan Dan Piponi bir makalesinde

Bir ADT olarak ortaya çıkan genişleme fiziksel olarak orijinal dilbilgisinden daha fazla bellek almaz - aslında biraz daha az sürer, çünkü Harita omurgası için ekstra log faktörüne ihtiyaç duymaz ve depolanması gerekmez tüm dizeleri.

Naif genişlemenin aksine, loeb'düğüm'ü bağlamama' ve aynı terminal olmayan tüm oluşumlar için thunks paylaşmama izin verin.

Tüm bunların teorisine daha fazla dalmak istiyorsanız, bunun RHSbir temel fonksiyona dönüştürülebileceğini görebiliriz :

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

ve sonra M tipim bunun sabit noktası Functor.

M a ~ Mu (RHS a)

ise G aseçilen bir dizgiden ve dizgeden -e bir haritadan oluşacaktır (RHS String a).

Daha sonra genişletebilirsiniz Giçine Mtembel genişletilmiş dizeleri haritasında girişi yukarı arama yoluyla.

Bu, data-reifypakette yapılanın, böyle bir temel functoru alabilen ve bunun gibi Mahlaki eşdeğerini gibi bir şeyden alabilen ikili G. Terminal olmayan isimler için temel olarak sadece bir Int.

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

ve bir birleştirici sağlayın

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

bu, keyfi bir matristen bir grafik (MatrixGrammar) elde etmek için yukarıdaki veri türlerinde uygun bir örnekle kullanılabilir. Aynı ancak ayrı olarak saklanan çeyreklerin tekilleştirilmesini yapmaz, ancak orijinal grafikte bulunan tüm paylaşımları kurtarır.


8

Haskell'de, String türü, bir vektör veya dizi değil, normal bir Haskell Char listesi olan [Char] için bir takma addır . Char, tek bir Unicode karakter içeren bir türdür. Dize değişmezleri, bir dil uzantısı kullanmazsanız, Dize türünün değerleridir.

Dize çok kompakt veya başka türlü etkili bir temsil olmadığını yukarıdan tahmin edebilirsiniz düşünüyorum. Dizeler için ortak alternatif gösterimler Data.Text ve Data.ByteString tarafından sağlanan türleri içerir.

Ekstra kolaylık sağlamak için, Data.ByteString.Char8 tarafından sağlanan gibi alternatif bir dize türünün temsili olarak dize değişmezlerini kullanabilmeniz için -XOverloadedStrings kullanabilirsiniz. Bu, dizeleri tanımlayıcı olarak uygun bir şekilde kullanmanın alan açısından en verimli yoludur.

Int'ye gelince, sabit genişlikli bir tiptir, ancak değerlerini tutacak kadar geniş olması dışında ne kadar geniş olduğu konusunda bir garanti yoktur [-2 ^ 29 .. 2 ^ 29-1]. Bu, en az 32 bit olduğunu gösterir, ancak 64 bit olduğunu dışlamaz. Data.Int, belirli bir genişliğe ihtiyacınız varsa kullanabileceğiniz daha belirli türlere, Int8-Int64'e sahiptir.

Bilgi eklemek için düzenleyin

Haskell'in anlamının her iki şekilde veri paylaşımı hakkında hiçbir şey belirtmediğine inanmıyorum. İki String değişmezinin veya herhangi bir yapılandırılmış verinin ikisinin bellekteki aynı 'standart' nesneye başvurmasını beklememelisiniz. Eğer inşa edilmiş bir değeri yeni bir isme (let, desen eşleşmesi vb. İle) bağlayacak olsaydınız, her iki isim de büyük olasılıkla aynı verilere atıfta bulunacaktı, ancak değişmez olup olmamaları, Haskell verileri.

Depolama verimliliği uğruna, temelde her birinin kanonik bir temsilini bir tür arama tablosunda, tipik olarak bir karma tabloda saklayan dizeleri stajyerleştirebilirsiniz . Bir nesneyi staj yaptığınızda, bunun için bir tanımlayıcı alırsınız ve bu tanımlayıcıları, dizelerle yapabileceğinizden çok daha ucuz olup olmadıklarını görmek için diğerleriyle karşılaştırabilirsiniz ve genellikle çok daha küçüktürler.

Staj yapan bir kütüphane için https://github.com/ekmett/intern/ adresini kullanabilirsiniz.

Çalışma zamanında hangi tamsayı boyutunun kullanılacağına karar verirken, somut sayısal türler yerine İntegral veya Num tipi sınıflara dayanan kod yazmak oldukça kolaydır. Tür çıkarımı size otomatik olarak yapabileceği en genel türleri verecektir. Daha sonra, başlangıç ​​kurulumunu yapmak için çalışma zamanında birini seçebileceğiniz belirli sayısal türlere açıkça daraltılmış türlerle birkaç farklı işleviniz olabilir ve daha sonra diğer tüm polimorfik işlevlerin herhangi biri üzerinde aynı şekilde çalışacaktır. Örneğin:

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

int16Constructor = polyConstructor
int32Constructor = polyConstructor

Edit : Staj hakkında daha fazla bilgi

Yalnızca dizeleri staj yapmak istiyorsanız, bir dizeyi (tercihen bir Metin veya ByteString) ve küçük bir tamsayıyı birlikte saran yeni bir tür oluşturabilirsiniz.

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

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

Intern 'in yaptığı, metinlerin anahtarlar ve InternedStrings değerlerinin olduğu zayıf referanslı HashMap dizesini aramaktır. Bir eşleşme bulunursa, 'stajyer' değeri döndürür. Değilse, orijinal Metin ve benzersiz bir tamsayı kimliği ile yeni bir InternedString değeri oluşturur (bu yüzden MonadIO kısıtlamasını dahil ettim; benzersiz kimliği almak için bir Devlet monad veya güvenli olmayan bir işlem kullanabilir; birçok olasılık vardır) ve iade etmeden önce haritaya kaydeder.

Şimdi tamsayı kimliğine göre hızlı bir karşılaştırma elde edersiniz ve her benzersiz dizenin yalnızca bir kopyasına sahip olursunuz.

Edward Kmett'in staj kütüphanesi aynı prensibi aşağı yukarı çok daha genel bir şekilde uygular, böylece tüm yapılandırılmış veri terimleri özetlenir, benzersiz bir şekilde saklanır ve hızlı bir karşılaştırma işlemi yapılır. Bu biraz ürkütücü ve özellikle belgelenmemiş, ama sorarsanız yardım etmeye istekli olabilir; ya da yeterli olup olmadığını görmek için önce kendi dize stajyer uygulamasını deneyebilirsiniz.


Şimdiye kadar cevabınız için teşekkürler. Çalışma zamanında hangi int boyutunu kullanmamız gerektiğini belirlemek mümkün müdür? Umarım başkası kopyalarla ilgili sorun hakkında bilgi verebilir :)
Dennis Ich

Eklenen bilgiler için teşekkürler. Oraya bir göz atacağım. Sadece doğru yapmak için bahsettiğimiz bu tanımlayıcılar, karma olan ve karşılaştırılabilecek bir referans gibi bir şey mi? Bununla kendin mi çalıştın? Belki bununla nasıl "daha karmaşık" olduğunu söyleyebilir misiniz çünkü ilk bakışta ben gramer tanımlamak sonra çok dikkatli olmak zorunda gibi görünüyor;)
Dennis Ich

1
Bu kütüphanenin yazarı, kaliteli çalışmalarıyla bilinen çok gelişmiş bir Haskell kullanıcısıdır, ancak bu kütüphaneyi kullanmadım. Yalnızca dizeleri değil, herhangi bir yapılandırılmış veri türünde temsil paylaşımını saklayacak ve temsil edebilecek çok genel amaçlı bir "hash eksileri" uygulamasıdır . Sizinki gibi bir sorun için onun örnek dizinine bakın ve eşitlik işlevlerinin nasıl uygulandığını görebilirsiniz.
Levi Pearson
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.