ประสิทธิภาพหน่วยความจำของ 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 ของเราจะบันทึกเฉพาะชื่อ String เพื่อพิจารณาโปรดักชั่นถัดไปและสิ่งที่เราไม่ทราบที่นี่คือวิธีที่ 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

นี่คือคำว่า "Terminal" เป็นการทำให้เข้าใจผิดเล็กน้อยเพราะจริงๆแล้วการผลิตที่มีเทอร์มินัลอยู่ทางด้านขวามือ เมทริกซ์เดียวกัน:

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

และคำถามที่คล้ายกัน: การผลิตจะถูกบันทึกไว้ภายในโดย Haskell บ่อยแค่ไหน? อาจเป็นไปได้ว่าเราจะส่งชื่อในโปรดักชั่นถ้าเราไม่ต้องการ แต่ตอนนี้เราไม่แน่ใจเกี่ยวกับเรื่องนี้

สมมติว่าเรามีไวยากรณ์ที่มีการผลิตประมาณ 1,000 รายการ วิธีใดที่จะใช้หน่วยความจำน้อยลง

ในที่สุดคำถามเกี่ยวกับจำนวนเต็มใน Haskell: ขณะนี้เรากำลังวางแผนที่จะมีชื่อเป็น Strings แต่เราสามารถเปลี่ยนเป็นชื่อจำนวนเต็มได้อย่างง่ายดายเพราะด้วย 1,000 โปรดักชั่นเราจะมีชื่อที่มีมากกว่า 4 ตัวอักษร (ซึ่งฉันถือว่าเป็น 32 บิต) Haskell จัดการเรื่องนี้อย่างไร Int 32 บิตและจำนวนเต็มเสมอจัดสรรหน่วยความจำที่ต้องการจริงๆหรือไม่?

ฉันยังอ่านผ่านสิ่งนี้: การทดสอบการหาค่าความหมายของค่าอ้างอิง / อ้างอิงของ Haskell - แต่ฉันไม่สามารถเข้าใจได้ว่าสิ่งนั้นมีความหมายสำหรับเราอย่างไร - ฉันเป็นเด็กจาวาที่มีความจำเป็นมากกว่า

คำตอบ:


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อธิบายไว้ในบทความโดย Dan Piponi

การขยายตัวที่เกิดขึ้นในขณะที่ ADT ใช้หน่วยความจำไม่มากไปกว่าไวยากรณ์ดั้งเดิม - ในความเป็นจริงมันใช้เวลาน้อยลงเพราะไม่ต้องการ log-factor พิเศษสำหรับกระดูกสันหลังของ Map และไม่จำเป็นต้องเก็บ สตริงที่ทั้งหมด

ซึ่งแตกต่างจากการขยายตัวไร้เดียงสาการใช้loebให้ฉัน 'ผูกปม' และแบ่งปัน thunks สำหรับการเกิดขึ้นทั้งหมดที่ไม่ใช่ขั้วเดียวกัน

หากคุณต้องการจุ่มลงในทฤษฎีทั้งหมดนี้เราจะเห็นว่าRHSสามารถกลายเป็น functor ฐาน:

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แพ็คเกจซึ่งสามารถรับฟังนักแสดงฐานและสิ่งที่ชอบMและฟื้นฟูความเท่าเทียมทางศีลธรรมของคุณGจากมัน Intพวกเขาใช้เป็นชนิดที่แตกต่างกันสำหรับชื่อที่ไม่ใช่ขั้วที่เป็นพื้นเพียง

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

และจัดให้มี combinator

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

ซึ่งสามารถใช้กับอินสแตนซ์ที่เหมาะสมกับชนิดข้อมูลข้างต้นเพื่อรับกราฟ (MatrixGrammar) จากเมทริกซ์โดยพลการ มันจะไม่ทำการลดความซ้ำซ้อนของ Quadrants ที่เหมือนกัน แต่แยกเก็บไว้ แต่มันจะกู้คืนการแบ่งปันทั้งหมดที่มีอยู่ในกราฟต้นฉบับ


8

ใน Haskell, ชนิดสตริงเป็นนามแฝงที่ [Char] ซึ่งเป็น Haskell ปกติรายการของ 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 ระบุอะไรเกี่ยวกับการแบ่งปันข้อมูลด้วยวิธีใด คุณไม่ควรคาดหวังว่าจะมีตัวอักษร String สองตัวหรือสองข้อมูลที่สร้างขึ้นเพื่ออ้างถึงวัตถุ 'มาตรฐาน' ในหน่วยความจำ หากคุณต้องผูกค่าที่สร้างไว้กับชื่อใหม่ (พร้อมให้, จับคู่รูปแบบและอื่น ๆ ) ทั้งสองชื่อมักจะอ้างถึงข้อมูลเดียวกัน แต่ไม่ว่าพวกเขาจะทำหรือไม่ไม่สามารถมองเห็นได้เนื่องจากลักษณะไม่เปลี่ยนรูปของ ข้อมูล Haskell

เพื่อประสิทธิภาพในการจัดเก็บข้อมูลคุณสามารถฝึกงานสตริงซึ่งเป็นตัวแทนการเก็บรักษาแบบบัญญัติของแต่ละรายการในตารางการค้นหาบางประเภทซึ่งโดยปกติจะเป็นตารางแฮช เมื่อคุณฝึกงานกับวัตถุคุณจะได้ descriptor กลับมาและคุณสามารถเปรียบเทียบ descriptor เหล่านั้นกับคนอื่น ๆ เพื่อดูว่าพวกมันเหมือนกันราคาถูกกว่าที่คุณจะใช้สตริงและพวกมันก็เล็กกว่ามาก

สำหรับห้องสมุดที่ฝึกงานคุณสามารถใช้https://github.com/ekmett/intern/

สำหรับการตัดสินใจว่าจะใช้ขนาดจำนวนเต็มในเวลาทำงานมันค่อนข้างง่ายในการเขียนโค้ดที่ขึ้นอยู่กับคลาสชนิด Integral หรือ Num แทนประเภทตัวเลขที่เป็นรูปธรรม การอนุมานประเภทจะให้ประเภททั่วไปที่สุดที่คุณสามารถทำได้โดยอัตโนมัติ จากนั้นคุณสามารถมีฟังก์ชั่นที่แตกต่างกันสองสามอย่างที่มีประเภทลดลงอย่างชัดเจนเป็นประเภทตัวเลขเฉพาะที่คุณสามารถเลือกอย่างใดอย่างหนึ่งในขณะทำการตั้งค่าเริ่มต้นและหลังจากนั้นฟังก์ชั่น polymorphic อื่น ๆ ทั้งหมดจะทำงานเหมือนกัน เช่น:

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

สิ่งที่ 'ฝึกงาน' ทำคือค้นหาสตริงใน HashMap อ้างอิงอ่อนโดยที่ Texts เป็นกุญแจและ InternedStrings เป็นค่า หากพบการแข่งขัน 'ฝึกงาน' จะส่งกลับค่า ถ้าไม่ใช่มันจะสร้างค่า InternedString ใหม่พร้อมข้อความต้นฉบับและรหัสจำนวนเต็มที่ไม่ซ้ำกัน (ซึ่งเป็นสาเหตุที่ฉันรวมข้อ จำกัด ของ MonadIO มันสามารถใช้ Monad ของรัฐหรือการดำเนินการที่ไม่ปลอดภัยแทนเพื่อรับรหัสเฉพาะ; และเก็บไว้ในแผนที่ก่อนส่งคืน

ตอนนี้คุณจะได้รับการเปรียบเทียบอย่างรวดเร็วตามจำนวนเต็มและมีสำเนาที่ไม่ซ้ำกันของสตริงเท่านั้น

ห้องสมุดฝึกงานของ Edward Kmett ใช้หลักการเดียวกันมากขึ้นหรือน้อยลงในวิธีทั่วไปที่มากขึ้นเพื่อให้คำศัพท์โครงสร้างข้อมูลทั้งหมดถูกแฮชจัดเก็บไม่ซ้ำกันและให้การเปรียบเทียบที่รวดเร็ว มันค่อนข้างน่ากลัวและไม่ได้จัดทำเป็นเอกสารโดยเฉพาะ แต่เขาอาจเต็มใจช่วยถ้าคุณถาม หรือคุณอาจลองใช้การฝึกงานสตริงของคุณเองก่อนเพื่อดูว่ามันช่วยได้เพียงพอหรือไม่


ขอบคุณสำหรับคำตอบของคุณ เป็นไปได้ไหมที่เราจะใช้ขนาด int ที่ runtime? หวังว่าคนที่ผมอื่นสามารถให้การป้อนข้อมูลบางอย่างเกี่ยวกับปัญหาที่เกิดขึ้นกับสำเนา :)
เดนนิส Ich

ขอบคุณสำหรับข้อมูลที่เพิ่มเข้ามา ฉันจะดูที่นั่น เพื่อให้ถูกต้องคำอธิบายนี้ที่คุณกำลังพูดถึงเป็นสิ่งที่ต้องการอ้างอิงที่ได้รับการแฮชและสามารถเปรียบเทียบได้? คุณทำงานกับตัวเองหรือไม่? คุณสามารถอาจจะบอกว่า "มีความซับซ้อนมากขึ้น" ที่จะได้รับกับเรื่องนี้เพราะในลักษณะแรกดูเหมือนว่าฉันจะต้องระมัดระวังเป็นอย่างมากแล้วกับการกำหนดไวยากรณ์นั้น)
เดนนิส Ich

1
ผู้เขียนของไลบรารีนั้นเป็นผู้ใช้ Haskell ขั้นสูงที่รู้จักกันในงานคุณภาพ แต่ฉันไม่ได้ใช้ห้องสมุดเฉพาะนั้น มันเป็นอย่างมากวัตถุประสงค์ทั่วไป "ข้อเสียกัญชา" การดำเนินงานซึ่งจะจัดเก็บและอนุญาตให้มีการใช้งานร่วมกันเป็นตัวแทนในการใด ๆชนิดของข้อมูลที่สร้างขึ้นไม่เพียงสตริง ดูไดเรกทอรีตัวอย่างของเขาเพื่อค้นหาปัญหาที่คล้ายกับคุณและคุณสามารถดูว่าการทำงานของความเสมอภาคนั้นถูกนำไปใช้อย่างไร
Levi Pearson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.