Haskell 메모리 효율성-더 나은 방법은 무엇입니까?


11

우리는 수정 된 2 차원 문법 구문에 기초하여 행렬 압축 라이브러리를 구현하고 있습니다. 이제 데이터 유형에 대한 두 가지 접근 방식이 있습니다. 메모리 사용시 어떤 것이 더 좋을까요? (우리는 무언가를 압축하고 싶다).

문법에는 정확히 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에서 한 번, 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의 정수에 대한 질문 : 현재 우리는 이름을 문자열로 사용할 계획입니다. 그러나 우리는 1000 개의 프로덕션으로 4 자 이상의 이름을 가질 것이기 때문에 정수 이름으로 쉽게 전환 할 수 있습니다 (32 비트라고 가정 함). Haskell은이를 어떻게 처리합니까? Int는 항상 32 비트이며 Integer는 실제로 필요한 메모리를 할당합니까?

나는 또한 이것을 읽었습니다 : Haskell의 값 / 참조 의미론의 테스트 개발 -그러나 그것이 우리에게 정확히 무엇을 의미하는지 알 수 없습니다-나는 명령형 자바 아이보다 좋은 기능성 프로그래머입니다 : 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.

loebDan Piponi의 기사에 설명되어 있습니다 .

결과적으로 ADT로서의 확장은 원래 문법보다 더 많은 메모리를 필요로하지 않습니다. 실제로 맵 스파인에 대한 추가 로그 팩터가 필요하지 않으며 저장하지 않아도되므로 상당히 적습니다. 문자열.

순진한 확장과 달리를 사용 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).

우리는 확장 할 수 있습니다 G으로 M느리게 확장 문자열의지도에서 해당 항목을 조회하여.

이것은 data-reify패키지 에서 수행되는 것의 이중이며 , 그러한 기본 functor를 가져 와서 그것과 M동등한 도덕적 인 것을 회복 할 G수 있습니다. 비 터미널 이름에는 다른 유형을 사용하는데 기본적으로는 단지 Int.

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

콤비 네이터를 제공

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

위의 데이터 유형에서 적절한 인스턴스와 함께 사용하여 임의 행렬에서 그래프 (MatrixGrammar)를 얻을 수 있습니다. 동일하지만 별도로 저장된 사분면의 중복 제거는 수행하지 않지만 원래 그래프에있는 모든 공유를 복구합니다.


8

Haskell에서 문자열 유형은 [Char]의 별칭으로 , 벡터 또는 배열이 아닌 Char 의 일반 Haskell 목록 입니다. Char는 단일 유니 코드 문자를 포함하는 유형입니다. 언어 리터럴은 언어 확장을 사용하지 않는 한 문자열 유형의 값입니다.

위의 문자열에서 String이 매우 작거나 효율적인 표현이 아니라고 생각할 수 있습니다. 문자열에 대한 일반적인 대체 표현에는 Data.Text 및 Data.ByteString에서 제공하는 유형이 포함됩니다.

추가 편의를 위해 -XOverloadedStrings를 사용하여 문자열 리터럴을 Data.ByteString.Char8에서 제공하는 대체 문자열 유형의 표현으로 사용할 수 있습니다. 문자열을 식별자로 편리하게 사용하는 가장 공간 효율적인 방법 일 것입니다.

Int까지는 고정 너비 유형이지만 [-2 ^ 29 .. 2 ^ 29-1] 값을 보유하기에 충분히 넓어야한다는 점을 제외하고는 너비가 넓다는 보장이 없습니다. 이것은 적어도 32 비트임을 암시하지만 64 비트임을 배제하지는 않습니다. Data.Int에는 특정 너비가 필요한 경우 사용할 수있는보다 구체적인 유형 인 Int8-Int64가 있습니다.

정보를 추가하려면 편집

나는 Haskell의 의미가 데이터 공유에 대해 어떤 식 으로든 지정한다고 생각하지 않습니다. 메모리에서 동일한 '정규'객체를 참조하기 위해 두 개의 문자열 리터럴 또는 구성된 데이터 중 두 개를 기 대해서는 안됩니다. 생성 된 값을 새 이름 (예 : 패턴 일치 등)에 바인딩하는 경우 두 이름 모두 동일한 데이터를 참조 할 가능성이 있지만 불변의 특성으로 인해 실제로 표시되지 않는지 여부는 확실하지 않습니다. 하스켈 데이터.

스토리지 효율성을 위해 문자열을 인턴 할 수 있습니다.이 문자열은 본질적으로 해시 테이블과 같은 일종의 룩업 테이블에 각각의 정규 표현을 저장합니다. 객체를 인턴하면 객체에 대한 디스크립터를 다시 가져 와서 해당 디스크립터를 다른 디스크립터와 비교하여 문자열보다 훨씬 저렴하고 훨씬 더 작은 지 확인할 수 있습니다.

interning을 수행하는 라이브러리의 경우 https://github.com/ekmett/intern/

런타임에 사용할 정수 크기를 결정하는 경우 구체적인 숫자 유형 대신 정수 또는 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'은 텍스트가 키이고 InternedStrings가 값인 약한 참조 HashMap에서 문자열을 조회합니다. 일치하는 것이 있으면 'intern'은 값을 반환합니다. 그렇지 않은 경우 원래 텍스트와 고유 한 정수 ID를 사용하여 새로운 InternedString 값을 생성합니다 (따라서 MonadIO 제약 조건을 포함 시켰습니다. 고유 한 ID를 얻기 위해 State 모나드 또는 안전하지 않은 작업을 사용할 수 있습니다. 여러 가지 가능성이 있습니다) 반환하기 전에지도에 저장합니다.

이제 정수 ID를 기반으로 빠른 비교를 수행하고 각 고유 문자열의 사본 하나만 보유합니다.

Edward Kmett의 인턴 라이브러리는 전체적으로 구조화 된 데이터 용어가 해시되고 고유하게 저장되며 빠른 비교 작업을 수행 할 수 있도록 거의 동일한 방식으로 훨씬 일반적인 방식으로 적용됩니다. 그것은 조금 어려우며 특별히 문서화되지는 않았지만, 당신이 물어 보면 기꺼이 도와 줄 것입니다. 또는 자신의 문자열 인턴 구현을 먼저 시도하여 그것이 충분히 도움이되는지 확인할 수 있습니다.


지금까지 답변 해 주셔서 감사합니다. 런타임에 사용해야하는 int 크기를 결정할 수 있습니까? 다른 사람이 사본의 문제에 대한 정보를 줄 수 있기를 바랍니다. :)
Dennis Ich

추가 된 정보에 감사드립니다. 나는 거기를 살펴볼 것이다. 바로 설명하기 위해이 설명자가 u가 해시하고 비교할 수있는 참조와 같은 것입니까? 이것을 당신 자신과 함께 했습니까? 어쩌면 문법을 정의 할 때 매우 조심해야한다고 생각하기 때문에 "더 복잡한"방법을 말할 수 있습니까?)
Dennis Ich

1
그 라이브러리의 저자는 양질의 작업으로 잘 알려진 고급 Haskell 사용자이지만 해당 라이브러리를 사용하지 않았습니다. 매우 일반적인 목적의 "해시 콘 (hash cons)"구현으로, 문자열뿐만 아니라 생성 된 모든 데이터 유형으로 표현 공유를 저장하고 허용합니다 . 그의 예제 디렉토리에서 당신과 같은 문제에 대해 살펴보고 평등 함수가 어떻게 구현되는지 볼 수 있습니다.
Levi Pearson
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.