Haskell内存效率-哪种方法更好?


11

我们正在基于修改后的二维语法语法实现矩阵压缩库。现在,我们对数据类型有两种方法-如果使用内存,哪种方法更好?(我们要压缩一些东西;))。

语法包含恰好有4个Productions的NonTerminals或右侧的Terminal。我们将需要Productions的名称来进行相等性检查和语法最小化。

首先:

-- | 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中,一次在b中4次,一次在生产中,一次在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位并且Integer分配它真正需要的内存?

我还读了以下内容:对Haskell的值/引用语义进行精心设计的测试 -但我无法弄清楚这对我们真正意味着什么-我更是命令式的Java子,然后是优秀的函数式程序员:P

Answers:


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

loebDan Piponi的一篇文章中有描述

作为ADT所得到的扩展在物理上不会比原始语法占用更多的内存-实际上,所需的内存要少得多,因为它不需要Map主干的额外对数因子,并且不需要存储所有的弦。

与天真的扩展不同,使用loeb让我“打结”并针对同一非终结点的所有出现共享代码。

如果您想更多地了解所有这些理论,我们可以看到RHS可以将其转变为基本函子:

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

然后我的M型就是它的定点Functor

M a ~ Mu (RHS a)

G a由选定的字符串和从字符串到的映射组成(RHS String a)

然后,我们可以扩展GM通过查找了在地图上懒洋洋地扩大字符串的条目。

这是data-reify包装中完成的工作的双重对,可以使用这样的基本函子,以及类似的东西,并从中M恢复您的道德等值G。他们为非终端名称使用不同的类型,基本上只是一个Int

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

并提供一个组合器

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

可以与上述数据类型上的适当实例一起使用,以从任意矩阵中获取图形(MatrixGrammar)。它不会对相同但分开存储的象限进行重复数据删除,但是它将恢复原始图形中存在的所有共享。


8

在Haskell中,String类型是[Char]的别名,它是Char的常规Haskell 列表,而不是向量或数组。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的语义能以任何一种方式指定有关数据共享的任何内容。您不应期望两个String文字或任何构造数据中的两个引用内存中的同一“规范”对象。如果要将构造的值绑定到新名称(使用let,模式匹配等),则这两个名称很可能都引用相同的数据,但是由于它们的不变性,它们是否确实是不可见的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中查找字符串,其中Text是键,而InternedStrings是值。如果找到匹配项,则“ intern”返回该值。如果没有,它将使用原始Text和唯一的整数ID创建一个新的InternedString值(这就是为什么我包含MonadIO约束;它可以使用State monad或一些不安全的操作来获取唯一的ID;这有很多可能性)并将其存储在地图中,然后再返回。

现在,您可以基于整数id进行快速比较,并且每个唯一字符串只有一个副本。

爱德华·克梅特(Edward Kmett)的内部库或多或少地应用了相同的原理,但更为通用,因此对整个结构化数据术语进行哈希处理,唯一存储并进行快速比较操作。这有点令人生畏,没有特别记录在案,但如果您提出要求,他可能愿意提供帮助。或者,您可以先尝试自己的字符串内部实现,看看它是否足够有用。


到目前为止,感谢您的回答。是否可以确定我们在运行时应该使用哪个int大小?我希望其他人可以提供有关副本问题的信息:)
Dennis Ich

感谢您添加的信息。我去那里看看。只是为了正确起见,您正在谈论的这个描述符是像引用一样经过散列并可以比较的东西?您是否以此为己任?您能否说出这有多“复杂”,因为乍一看似乎我必须非常小心然后再定义语法;)
Dennis Ich

1
该库的作者是一个非常高级的Haskell用户,以高质量的工作而闻名,但是我没有使用该特定的库。这是一个非常通用的“哈希缺点”实现,它将存储并允许在任何构造的数据类型中共享表示形式,而不仅仅是字符串。在他的示例目录中查找类似您的问题,您将看到如何实现相等函数。
维·皮尔森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.