Hiệu quả bộ nhớ Haskell - đó là cách tiếp cận tốt hơn?


11

Chúng tôi đang triển khai thư viện nén ma trận dựa trên cú pháp ngữ pháp hai chiều được sửa đổi. Bây giờ chúng ta có hai cách tiếp cận cho các loại dữ liệu của mình - cách nào sẽ tốt hơn trong trường hợp sử dụng bộ nhớ? (chúng tôi muốn nén một cái gì đó;)).

Các ngữ pháp chứa NonTermals với chính xác 4 Sản phẩm hoặc Terminal ở phía bên tay phải. Chúng tôi sẽ cần tên của Productions để kiểm tra bình đẳng và tối thiểu hóa ngữ pháp.

Thứ nhất:

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

Ở đây, dữ liệu RightHandSide của chúng tôi chỉ lưu tên Chuỗi để xác định các sản phẩm tiếp theo và điều chúng tôi không biết ở đây là cách Haskell lưu các chuỗi này. Ví dụ: ma trận [[0, 0], [0, 0]] có 2 sản phẩm:

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

Vì vậy, câu hỏi ở đây là chuỗi "A" có thực sự được lưu không? Một lần trong aString, 4 lần trong b và một lần trong sản xuất hoặc chỉ một lần trong aString và những lần khác chỉ giữ các tham chiếu "rẻ hơn"?

Thư hai:

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

type ProductionMap = Map String Production

ở đây thuật ngữ "Terminal" là một chút sai lệch bởi vì thực sự nó là sản phẩm có một thiết bị đầu cuối là phía bên tay phải. Ma trận giống nhau:

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

và câu hỏi tương tự: mức độ thường xuyên được sản xuất bởi Haskell? Có thể chúng tôi sẽ bỏ tên bên trong sản phẩm nếu chúng tôi không cần chúng, nhưng chúng tôi không chắc chắn ngay bây giờ về điều này.

Vì vậy, giả sử chúng ta có một ngữ pháp với khoảng 1000 sản phẩm. Cách tiếp cận nào sẽ tiêu thụ ít bộ nhớ hơn?

Cuối cùng, một câu hỏi về số nguyên trong Haskell: Hiện tại chúng tôi đang có kế hoạch đặt tên là Chuỗi. Nhưng chúng ta có thể dễ dàng chuyển sang tên nguyên vì với 1000 sản phẩm, chúng ta sẽ có tên có hơn 4 ký tự (mà tôi giả sử là 32 bit?). Làm thế nào để Haskell xử lý này. Có phải một Int luôn là 32 Bit và Integer phân bổ bộ nhớ mà nó thực sự cần?

Tôi cũng đã đọc qua điều này: Phát minh thử nghiệm về ngữ nghĩa tham chiếu / giá trị của Haskell - nhưng tôi không thể hiểu chính xác điều đó có ý nghĩa gì đối với chúng tôi - Tôi là một đứa trẻ java bắt buộc hơn là lập trình viên chức năng tốt: P

Câu trả lời:


7

Bạn có thể mở rộng ngữ pháp ma trận của mình thành một ADT với chia sẻ hoàn hảo với một chút mánh khóe:

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

Ở đây tôi đã khái quát các ngữ pháp của bạn để cho phép bất kỳ loại dữ liệu nào, không chỉ Int, và tabulatesẽ lấy ngữ pháp và mở rộng nó bằng cách tự gấp nó lại bằng cách sử dụng loeb.

loebđược mô tả trong một bài viết của Dan Piponi

Việc mở rộng kết quả như một ADT về mặt vật lý không chiếm nhiều bộ nhớ hơn ngữ pháp gốc - thực tế nó tốn ít công bằng hơn, vì nó không cần thêm yếu tố log cho cột sống Bản đồ và không cần lưu trữ các chuỗi ở tất cả.

Không giống như bản mở rộng ngây thơ, sử dụng loebcho phép tôi 'thắt nút' và chia sẻ các thunks cho tất cả các lần xuất hiện của cùng một thiết bị đầu cuối.

Nếu bạn muốn nhúng nhiều hơn vào lý thuyết của tất cả những điều này, chúng ta có thể thấy điều đó RHScó thể được biến thành một functor cơ sở:

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

và sau đó loại M của tôi chỉ là điểm cố định của điều đó Functor.

M a ~ Mu (RHS a)

trong khi G asẽ bao gồm một chuỗi được chọn và một bản đồ từ các chuỗi đến (RHS String a).

Sau đó chúng tôi có thể mở rộng Gthành Mbởi tra cứu lên các mục trong bản đồ về chuỗi mở rộng một cách lười biếng.

Đây là loại kép của những gì được thực hiện trong data-reifygói, có thể lấy một functor cơ sở như vậy, và một cái gì đó giống như Mvà phục hồi tương đương đạo đức của bạn Gtừ nó. Họ sử dụng một loại khác cho các tên không phải thiết bị đầu cuối, về cơ bản chỉ là một Int.

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

và cung cấp một tổ hợp

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

có thể được sử dụng với một thể hiện thích hợp trên các kiểu dữ liệu trên để đưa một biểu đồ (MatrixGrammar) ra khỏi một ma trận tùy ý. Nó sẽ không lặp lại các góc phần tư giống nhau nhưng được lưu trữ riêng biệt, nhưng nó sẽ phục hồi tất cả các chia sẻ có trong biểu đồ gốc.


8

Trong Haskell, loại Chuỗi là bí danh cho [Char], là danh sách Haskell thông thường của Char, không phải là vectơ hoặc mảng. Char là một loại chứa một ký tự Unicode. Chuỗi ký tự là, trừ khi bạn sử dụng phần mở rộng ngôn ngữ, các giá trị của loại Chuỗi.

Tôi nghĩ rằng bạn có thể đoán từ trên rằng String không phải là một đại diện rất nhỏ gọn hoặc hiệu quả. Các biểu diễn thay thế phổ biến cho các chuỗi bao gồm các loại được cung cấp bởi Data.Text và Data.ByteString.

Để thuận tiện hơn, bạn có thể sử dụng -XOverloadedStrings để bạn có thể sử dụng chuỗi ký tự chuỗi làm biểu diễn của một loại chuỗi thay thế, như Data.ByteString.Char8 cung cấp. Đó có lẽ là cách hiệu quả nhất về không gian để sử dụng thuận tiện các chuỗi làm định danh.

Theo như Int, đây là loại có chiều rộng cố định, nhưng không có gì đảm bảo về độ rộng của nó ngoại trừ việc nó phải đủ rộng để giữ các giá trị [-2 ^ 29 .. 2 ^ 29-1]. Điều này cho thấy ít nhất là 32 bit, nhưng không loại trừ là 64 bit. Data.Int có một số loại cụ thể hơn, Int8-Int64, bạn có thể sử dụng nếu bạn cần một chiều rộng cụ thể.

Chỉnh sửa để thêm thông tin

Tôi không tin rằng ngữ nghĩa của Haskell chỉ định bất cứ điều gì về chia sẻ dữ liệu theo bất kỳ cách nào. Bạn không nên mong đợi hai chuỗi ký tự, hoặc hai trong số bất kỳ dữ liệu được xây dựng nào, đề cập đến cùng một đối tượng 'chính tắc' trong bộ nhớ. Nếu bạn đã liên kết một giá trị được xây dựng với một tên mới (với let, khớp mẫu, v.v.) thì cả hai tên đều có thể tham chiếu đến cùng một dữ liệu, nhưng liệu chúng có thực sự không hiển thị do tính chất bất biến của Dữ liệu Haskell.

Để đảm bảo hiệu quả lưu trữ, bạn có thể thực hiện các chuỗi, về cơ bản lưu trữ một biểu diễn chính tắc của từng chuỗi trong một bảng tra cứu nào đó, điển hình là bảng băm. Khi bạn thực hiện một đối tượng, bạn lấy lại một mô tả cho nó và bạn có thể so sánh các mô tả đó với các đối tượng khác để xem liệu chúng có rẻ hơn nhiều so với các chuỗi của bạn hay không và chúng thường nhỏ hơn nhiều.

Đối với thư viện thực tập, bạn có thể sử dụng https://github.com/ekmett/i INTERN /

Đối với việc quyết định sử dụng kích thước số nguyên nào trong thời gian chạy, việc viết mã khá dễ dàng phụ thuộc vào các lớp kiểu Integral hoặc Num thay vì các kiểu số cụ thể. Kiểu suy luận sẽ cung cấp cho bạn các loại chung nhất mà nó có thể tự động. Sau đó, bạn có thể có một vài hàm khác nhau với các kiểu được thu hẹp rõ ràng thành các kiểu số cụ thể mà bạn có thể chọn một trong các thời gian chạy để thực hiện thiết lập ban đầu và sau đó tất cả các hàm đa hình khác sẽ hoạt động giống nhau trên bất kỳ hàm nào. Ví dụ:

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

int16Constructor = polyConstructor
int32Constructor = polyConstructor

Chỉnh sửa : Thêm thông tin về thực tập

Nếu bạn chỉ muốn thực hiện các chuỗi, bạn có thể tạo một kiểu mới bao bọc một chuỗi (tốt nhất là một Văn bản hoặc ByteString) và một số nguyên nhỏ với nhau.

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

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

Những gì 'intern' làm là tìm kiếm chuỗi trong HashMap tham chiếu yếu trong đó Texts là khóa và InternedStrings là giá trị. Nếu một trận đấu được tìm thấy, 'intern' trả về giá trị. Nếu không, nó tạo ra một giá trị InternedString mới với Văn bản gốc và id số nguyên duy nhất (đó là lý do tại sao tôi đưa vào ràng buộc MonadIO; nó có thể sử dụng một trạng thái đơn nguyên hoặc một số thao tác không an toàn để lấy id duy nhất; có nhiều khả năng) và lưu nó trong bản đồ trước khi trả lại.

Bây giờ bạn có được một so sánh nhanh dựa trên id số nguyên và chỉ có một bản sao của mỗi chuỗi duy nhất.

Thư viện thực tập của Edward Kmett áp dụng cùng một nguyên tắc, ít nhiều, theo cách tổng quát hơn nhiều để toàn bộ thuật ngữ dữ liệu có cấu trúc được băm, lưu trữ duy nhất và đưa ra thao tác so sánh nhanh. Đó là một chút nản chí và không đặc biệt là tài liệu, nhưng anh ta có thể sẵn sàng giúp đỡ nếu bạn yêu cầu; hoặc bạn chỉ có thể thử thực hiện thực tập chuỗi của riêng bạn trước để xem nó có đủ không.


Cảm ơn câu trả lời của bạn cho đến nay. Có thể xác định kích thước int chúng ta nên sử dụng trong thời gian chạy? Tôi hy vọng người khác có thể đưa ra một số thông tin về vấn đề với các bản sao :)
Dennis Ich

Cảm ơn các thông tin bổ sung. Tôi sẽ xem ở đó. Chỉ cần làm cho đúng, mô tả này bạn đang nói đến là một cái gì đó giống như một tài liệu tham khảo được băm và có thể được so sánh? Bạn đã làm việc với chính mình này? Bạn có thể nói nó "phức tạp hơn" như thế nào không vì điều này thoạt nhìn có vẻ như tôi phải rất cẩn thận sau đó với việc xác định ngữ pháp;)
Dennis Ich

1
Tác giả của thư viện đó là một người dùng Haskell rất tiên tiến được biết đến với chất lượng công việc, nhưng tôi chưa sử dụng thư viện cụ thể đó. Đây là một triển khai "băm nhược điểm" có mục đích rất chung, sẽ lưu trữ và cho phép chia sẻ biểu diễn trong bất kỳ loại dữ liệu được xây dựng nào , không chỉ các chuỗi. Nhìn vào thư mục ví dụ của anh ấy để biết loại vấn đề giống như của bạn và bạn có thể thấy các hàm bằng được thực hiện như thế nào.
Levi Pearson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.