Haskell से Huffman code आधारित compression utility बनाना
(lazamar.github.io)- लगभग 150 लाइनों के Haskell में Huffman coding compressor लागू करते हुए, arbitrary binary files की encoding·decoding को constant memory में संभालने वाला structure दिखाता है
- Huffman code अक्सर आने वाली values को छोटी bit sequences देता है, और prefix-free code condition से decoder bit sequence को बिना ambiguity के interpret कर पाता है
- Implementation
FreqMap,HTree,CodeMapके जरिए frequency table से tree बनाता है, औरconcatMapव recursive decoder की lazy evaluation से result को incremental रूप से generate करता है - Binary files के लिए
Data.ByteString.Char8से bytes कोCharकी तरह handle किया जाता है; compressed result के आगे frequency table serialize की जाती है और bit sequence को byte units में padding करके store किया जाता है - Tests में War and Peace 3.2M से 1.9M हो गया, 106M
ghcupbinary 84M तक घटा, और maximum resident set size 300KB से कम observe हुआ
Huffman code compression का basic idea
- लक्ष्य Huffman coding का इस्तेमाल करके लगभग 150 लाइनों के Haskell में data compression utility implement करना है
- पूरा code GitHub repository पर public है
- Huffman code हर character या value को एक unique bit sequence assign करता है
- अक्सर आने वाली values छोटी bit sequence use करती हैं
- कम आने वाली values लंबी bit sequence use करती हैं
- common values original representation से कम bits में represent होती हैं, जिससे compression effect मिलता है
- Example
aaabमेंa = 1,b = 0रखने पर result1110बनता है- यह UTF-8 में 4 bytes लगने वाली string को आधे byte में represent करने का example है
Prefix-free code और Huffman tree
- Decoding ambiguity-free होनी चाहिए, तो कोई भी codeword किसी दूसरे codeword का prefix नहीं होना चाहिए
- उदाहरण के लिए
aaabcमेंa = 1,b = 10,c = 01assign करने पर101केacयाbaहोने में ambiguity हो सकती है
- उदाहरण के लिए
- Prefix-free code को full binary tree के रूप में बनाया जा सकता है
- सभी values को leaves पर रखा जाता है
- left edge को
1, right edge को0label किया जाता है - root से leaf तक का path उस value का codeword बनता है
- Huffman tree कम frequency वाली values को नीचे से group करते हुए ऊपर की ओर बनाने के तरीके से generate किया जाता है
- हर character को उसके occurrence count यानी weight के साथ node बनाया जाता है
- सबसे छोटे weight वाले दो nodes को tree में जोड़ा जाता है
- नए tree का weight दोनों nodes के weights का sum होता है
- यह तब तक repeat होता है जब तक सिर्फ एक tree न बच जाए
- इस process से ज्यादा frequent values root के करीब आ जाती हैं और उनके codewords छोटे हो जाते हैं
Haskell encoder structure
- Implementation के main types
Bit,Code,FreqMap,CodeMap,Weight,HTreeहैंBitOneयाZeroहैCode[Bit]हैFreqMapMap Char Intहै, जो हर character की occurrence count store करता हैCodeMapMap Char Codeहै, जो हर character का codeword store करता हैHTreeLeaf Weight CharयाFork Weight HTree HTreeहै
HTreeको weight के आधार पर comparable बनाया गया है ताकि tree generation process में sorting और insertion simple रहेcountFrequencystring में हर character की occurrence count calculate करता हैbuildTreeFreqMapको leaf list में बदलकर sort करता है, फिर सबसे छोटे दो nodes को repeatedly combine करके Huffman tree बनाता हैbuildCodestree traverse करते हुए left मेंOne, right मेंZeroजोड़कर हर character का codeword generate करता हैencode :: FreqMap -> String -> [Bit]FreqMapसे tree और code map बनाता है, और input string के हर character को codeword से replace करके bit list बनाता है
Lazy evaluation से incremental processing
- Encoding का core transformation
concatMap codeFor strहै- Conceptually यह
[Char]को[[Bit]]में बदलकर फिर[Bit]में flatten करने की process है - Haskell की lazy evaluation की वजह से यह पहले पूरे input को encode करके फिर combine करने जैसा operate नहीं करता
- Conceptually यह
- छोटी lists left से right की ओर process होती हैं और बड़ी result list में flatten होती हैं
- Result list का tail अभी evaluate नहीं हुआ thunk बना रहता है
- जब कोई needed value request होती है, तभी next part calculate होता है
- Decoder भी इसी तरह result incremental रूप से generate करता है
decode :: FreqMap -> [Bit] -> Stringbits के अनुसार tree में left या right जाता है- Leaf पर पहुंचने पर character output करता है और root से फिर शुरू करता है
- यह तब तक repeat होता है जब तक decoded characters की total count Huffman tree के weight के बराबर न हो जाए
- Decoder input bit list के end से नहीं, बल्कि character count के आधार पर रुकता है
- क्योंकि serialization stage में byte alignment के लिए आखिर में padding bits add किए जाते हैं
gofunction leaf पर पहुंचने पर known head वाली list और tail recursive call return करता है, इसलिए पूरी recursion खत्म होने से पहले result evaluate किया जा सकता है
Binary file handling और serialization
- Binary data को 256 possible bytes में से किसी एक के repeat होने वाले form के रूप में देखा जा सकता है
Data.ByteString.Char8ByteStringकोCharoperations से handle करने देता है, और सभीChar8-bit में truncate हो जाते हैं- इस property की वजह से text coder को ज्यादा बदले बिना binary data पर भी apply किया जा सकता है
- Compressed file में decoding के लिए जरूरी
FreqMapपहले आता है, उसके बाद encoded bit sequence आती है serializeFreqMapfrequency table को निम्न format में record करता है- Map length को
Word8के रूप में store करता है, लेकिन representable range adjustment के लिए 1 घटाकर store करता है - हर item में key को
Word8और frequency value को 64-bit big-endian integer के रूप में store किया जाता है
- Map length को
serializebinarypackage केPutmonad सेByteStringgenerate करता है- Bits को एक-एक करके पढ़कर एक byte fill करता है
- 8 bits भरने पर
putWord8से record करता है - Last byte में बची जगह
Zeroसे pad की जाती है
Deserialization और constant-memory strategy
deserializeFreqMapData.Binary.Getसे serialized frequency table पढ़ता है- पहले length पढ़कर उसमें 1 जोड़कर actual item count calculate करता है
- हर item से
Word8key और 64-bit frequency value पढ़करFreqMaprestore करता है
- बाकी पूरा input
Getसे process नहीं किया जाता; इसके बजायByteStringमें offset के बाद वाला हिस्सा निकालकर bit list में बदला जाता है deserialize(FreqMap, [Bit])return करता है, और[Bit]एक lazy list है जो तुरंत पूरी calculate नहीं होती- अगर इस list की length मांगी जाए तो पूरी list evaluate करनी पड़ेगी, इसलिए इससे बचना चाहिए
- पूरे input पर
Getuse न करने की वजह यह है कि monad का bind order force करता है- इससे ऐसी structure बनती है जिसमें पूरी input processing खत्म होने के बाद ही list return हो सकती है
- Constant-memory strategy यह है कि output bits लिखते समय input का सिर्फ next छोटा हिस्सा evaluate किया जाए
- कुछ
ByteStringevaluate होता है और file का वह piece पढ़ा जाता है - Processed output file में record होता है
- जिन input pieces और bit lists का अब reference नहीं रहता उन्हें garbage collector reclaim कर सकता है
- कुछ
FreqMapbyte basis पर maximum 256 items वाला होता है, इसलिए constant-size overhead में रहता है
File compression·decompression CLI
compressinput file को दो बार पढ़ता है- First pass में
FreqMapबनाता है - Second pass में उसी
FreqMapसे data encode करता है
- First pass में
- अगर file को सिर्फ एक बार पढ़कर वही reference
encodeको दें, तो frequency table बनने के बाद भी पूरे input file reference को बनाए रखना पड़ेगा, इसलिए पूरी file memory में रखनी पड़ेगी - दो बार पढ़ने वाला तरीका frequency table generation और encoding दोनों में processed memory को process के दौरान free होने देता है
decompresscompressed file पढ़करdeserializeसेFreqMapऔर bit list लेता है, फिरdecoderesult को file में save करता है- CLI ये arguments लेता है
compress FILE FILEdecompress FILE FILE
- चूंकि यह केवल GHC में included packages use करता है, इसलिए
cabalके बिनाghc -O2 Main.hs -o mainसे compile किया जा सकता है
Execution results और memory usage
- Tolstoy की War and Peace text file test में compression और decompression के बाद
diffresult identical है- Original
WarAndPeace.txt: 3.2M - Compressed file: 1.9M
- Decompressed file: 3.2M
- Size लगभग 40% घट गया
- Original
- बड़े binary file
ghcupपर भी compression और decompression काम करता है- Original
ghcup: 106M - Compressed file: 84M
- Decompressed file: 106M
- Compression time लगभग 15.173 seconds, decompression time लगभग 14.555 seconds है
- Original
+RTS -sके हिसाब सेghcupprocessing के दौरान maximum resident set size 300KB से कम है- दोनों processes execution में 10MB से कम memory use करते हैं
- Time कहां spend हुआ, यह अलग profile में देखा जा सकता है
आगे सुधार की संभावनाएं
- Implementation goal जितना हो सके उतनी simple और clear compression utility बनाना था; efficiency बढ़ाने के लिए ज्यादा complex implementation चाहिए
- Possible improvements ये हैं
- Multithreading: file sections को parallel decode करना, लेकिन arbitrary position पर codeword boundary पता नहीं चल सकती, इसलिए compressed file की शुरुआत में section boundaries और expected decoded size table add करनी होगी
- Single-pass encoding: सभी bytes की frequency 1 वाली initial frequency table से शुरू करना, और हर byte देखते समय पहले encode करना फिर frequency table update करना
- Canonical Huffman codes: decoding में tree को
O(log n)में traverse करने के बजाय code को vector index की तरह use करकेO(1)access target किया जा सकता है; related details Canonical Huffman code में हैं - Faster code generation: single-pass encoding में
CodeMapgeneration को काफी faster करना होगा, और tree बनाए बिना codewords generate करने का faster तरीका मौजूद है
- भविष्य में adaptive dictionary method LZ77 का इस्तेमाल करने पर Huffman code के साथ gzip implement किया जा सकता है
1 टिप्पणियां
Hacker News की राय
इस काम के लिए एक array-based in-place algorithm है, जो trees allocate करने और pointers follow करने का काम कम करता है
कॉलेज में जब tree-based approach सीखी थी, तब मुझे पता नहीं था कि कोई दूसरा तरीका भी है, और सोचता हूँ क्या दूसरों के साथ भी ऐसा ही था
tree वाला तरीका intuitive है और समझने में मदद करता है, लेकिन जहाँ compression सबसे ज़्यादा मायने रखता है, वहाँ आमतौर पर data बहुत होता है और उसे तेज़ चलाना होता है, इसलिए in-place array के रूप में संभालना ज़्यादा logical हो सकता है
In-Place Calculation of Minimum-Redundancy Codes, Moffat, Katajainen, 1995
http://hjemmesider.diku.dk/~jyrki/Paper/WADS95.pdf
Charles Bloom ने इसकी ज़ोरदार सिफारिश की थी और बाद में इस पर explanation भी जोड़ा था
https://cbloomrants.blogspot.com/2010/08/08-12-10-lost-huffm...
“कोई भी code word किसी दूसरे code word का prefix नहीं होना चाहिए, तभी ambiguity नहीं होगी” — यह बात strictly accurate नहीं है
तथाकथित uniquely decodable codes ambiguous नहीं होते, और वे prefix codes का superset हैं
एक आसान उदाहरण है prefix code को उलट देना. लेख के उदाहरण में यह
a 1,b 00,c 10होगाaका code,ccode का prefix है, लेकिन code sequence को उल्टे क्रम में process करें तो फिर भी बिना ambiguity के decode कर सकते हैं. ऐसा uniquely decodable code देखना दिलचस्प होगा जो न prefix code हो और न उसका reverseA 0,B 01,C 11रखें, और फिरa A 0,b BA 010,c BB 0101,d BC 0111,e C 11रखें, तो{a=0,b=010,c=0101,d=0111,e=11}मिलता हैपीछे से
0->Aजैसी uniquely decoding करने के बाद, आगे सेA->aजैसी uniquely decoding दोबारा कर सकते हैं, इसलिए यह साफ़ तौर पर uniquely decodable हैlength के लिहाज़ से यह optimal prefix code
{a=0,b=110,c=1110,d=1111,e=10}जैसा ही है, इसलिए समान probability distribution के लिए यह कई optimal codes में से एक हैसाथ ही
a=0,b=010की वजह से यह न prefix code है, न suffix code. दरअसल सामान्य तौर पर किसी भी दिशा में incremental decoding संभव नहीं होती, औरcee...ee?औरbee...ee?,?cc...ccaऔर?cc...ccbमें अंतर करने के लिए एक symbol पहचानने में भी असीमित lookahead की ज़रूरत पड़ सकती हैस्वतंत्र रूप से optimal prefix code और स्वतंत्र रूप से optimal suffix code को compose करने पर optimality हमेशा बनी रहती है या नहीं, यह मुझे नहीं पता, लेकिन degenerate 1:1 codes को छोड़कर मेरे दिमाग में आए सबसे सरल cases में यह ठीक काम करता है
a 101,b 1जैसा उदाहरण दिया जा सकता हैलेकिन यह खराब code है, क्योंकि हमेशा
a=1,b=0रखना बेहतर होगाKraft inequality बताती है कि code lengths के कौन-से sets uniquely decodable बनाए जा सकते हैं, और Huffman coding से ऐसे सारे sets हासिल किए जा सकते हैं. इसलिए अगर आप symbol coding कर रहे हैं, तो ANS या arithmetic coding जैसे किसी दूसरे तरीके पर जाने के अलावा non-prefix code इस्तेमाल करने की कोई वजह नहीं है
हालांकि यह नहीं पता कि optimal Huffman code जैसे length set वाला, लेकिन न prefix code और न उसका reverse यानी suffix code, ऐसा कोई uniquely decodable code मौजूद है या नहीं
समय लगाऊँ तो शायद https://en.wikipedia.org/wiki/Sardinas-Patterson_algorithm देखकर brute force से counterexample खोजूँगा, या algorithm के काम करने के तरीके से proof सोचूँगा
a 1,b 101कैसा रहेगा, यह सोचता हूँयह न prefix-free है, न suffix-free, लेकिन जब भी
0आता है, वहbके आने से correspond करता हैबेशक यह साफ़ तौर पर inefficient है, इसलिए आखिर सवाल यह है कि क्या ऐसा optimal code है जो न prefix-free हो न suffix-free
खोजने पर https://blog.plover.com/CS/udcodes.html में
a 0011,b 011,c 11,d 1110वाला uniquely decodable code example मिलता हैइसमें एकमात्र prefix relation बस इतना है कि
c,dका prefix है, इसलिए यह “लगभग” prefix-free जैसा है. अगर message1से शुरू होता है, तो पहला0खोजकर उसके पहले कितने1हैं—odd या even—यह देखकर शायद समझा जा सकता है कि यह uniquely decodable हैहालांकि यह किस probability distribution के लिए optimal है, यह दिखाने का तरीका याद नहीं, क्योंकि cryptography का ज्ञान काफ़ी rust हो चुका है
उदाहरण के लिए
100000000000000001में पहला codeaहै याc, यह जानने के लिए सभी 0s खत्म होने वाली जगह तक पूरा पढ़ना पड़ेगाइस लेख की तरह Haskell program बनाते हुए आगे बढ़ने वाले similar tutorials में, क्या monad transformers या lenses जैसी ज्यादा advanced features को cover करने वाली कोई material है?
Template Haskell, concurrency जैसी दूसरी advanced features भी cover होती हैं, और Haskell में SQL databases handle करने वाला chapter भी है
यह FRP library Rhine का tutorial है, और comments व tests अच्छी तरह लिखे गए हैं
Coursera के Scala-आधारित functional programming course में भी काफी मिलता-जुलता Huffman coding assignment है, और उसमें automatic grader भी है, इसलिए जो लोग खुद हल करके देखना चाहते हैं उनके लिए अच्छा है
https://www.coursera.org/learn/scala-functional-programming?...
आख़िरी बार मैंने Huffman code का इस्तेमाल MICMAC processor के macroprogram, यानी assembly text को न्यूनतम microcycles और न्यूनतम microinstructions में चलाने के लिए किया था
मैंने executed macroinstruction histogram से शुरुआत की थी, और जहाँ तक याद है, पहले C में interpreter लिखकर गिना था कि हर instruction कितनी बार execute होती है
उसके बाद मैंने एक incremental decoding microcode program बनाया, जो ज़रूरी ISA macro operations सभी को implement करता था। लगता है कि बनाया गया macroinstruction ISA byte-oriented नहीं, बल्कि bit-level था
असल दुनिया में यह धीमा और असुविधाजनक रहा होगा, लेकिन Huffman code का फायदा यह है कि value distribution के हिसाब से prefix depth adjust की जा सकती है, इसलिए 1-bit prefix की वजह से एक तरफ़ झुका हुआ code बनाने की ज़रूरत नहीं पड़ती
साथ ही, microprogram एक non-superscalar pipelined processor model था, इसलिए branch prediction भी संभालनी पड़ती थी। branch को गलत predict करने पर, सही branch के आगे propagate होने तक pipeline stall की वजह से cycles बर्बाद हो जाते हैं
https://rosettacode.org/wiki/Huffman_coding
लगता है Haskell प्रोग्रामर यहाँ इकट्ठा होंगे, इसलिए पूछना चाहता हूँ: आजकल Haskell, अगर कोई programmer optimization पर ध्यान देकर लिखे, तो कितना तेज़ है?
खासकर matrix operations जैसे numeric computing और SIMD से फायदा लेने वाले workloads की performance जानना चाहता हूँ
कई हिस्सों को एक consistent और अच्छी तरह organized program में जोड़ना आसान होना ही core बात है। यह सिर्फ tight loops के लिए नहीं, बल्कि पूरे program के लिए अहम है
Haskell का FFI अच्छा है, इसलिए जिन हिस्सों में मूल रूप से imperative optimization की ज़रूरत होती है, उन्हें garbage collection वाली language के बाहर ले जाया जा सकता है। अगर ऐसे हिस्सों को अच्छे types वाली library में wrap कर दें, तो type-match करने वाले Haskell code में कहीं भी उस raw performance का इस्तेमाल किया जा सकता है
Meta में high-performance Haskell applications बनाते समय हमने ऐसा ही किया था। सुंदर, बड़े और तेज़ Haskell programs लिखे, लेकिन कुछ specialized हिस्सों में C++ components लगाए। 99% समय Haskell side पर उन्हें ज्यादा उपयोगी applications में compose करने में लगा
फिर भी Haskell धीमी नहीं है। file में 1-bit की संख्या गिनने वाला छोटा program इसका उदाहरण हो सकता है
-msse4.2से compile करने पर यह hardwarepopcountinstruction का सही इस्तेमाल करता है, और 1GB input file को0m0,090sमें process करता है। MB में round करने पर heap usage 0 है-msse4.2के बिना compile करने पर0m0,293sलगता हैmatrix computation खुद नहीं आज़माया, लेकिन शुरुआत के लिए शायद
repa,accelerate,massivदेखूँगाhttps://hackage.haskell.org/package/repa
https://hackage.haskell.org/package/accelerate
https://hackage.haskell.org/package/massiv
https://gitlab.haskell.org/ghc/ghc/-/issues/7741
यह GHC 9.12 में आ सकता है। हालांकि target केवल 128-bit vectors होंगे, और जब तक दूसरे लोग contribute नहीं करते, यह मुख्यतः floating-point operations पर केंद्रित रहेगा
patch यहाँ है
https://gitlab.haskell.org/ghc/ghc/-/merge_requests/12860
एक स्तर पर host language का चुनाव बहुत मायने नहीं रखता। अगर आप सच में speed को लेकर serious हैं, तो computation वैसे भी बाहर offload कर देंगे
इसी वजह से AI code—जो दुनिया में सबसे ज्यादा compute resources इस्तेमाल करने वालों में हो सकता है—low-level compute libraries को छोड़कर Python में लिखा जा सकता है
सीधे जवाब दें तो GHC compiler बहुत अच्छा है। high-level code काफी अच्छा चलता है, और ज्यादातर real-world applications में performance bottleneck single-width operations vs SIMD जैसी चीज़ नहीं, बल्कि architecture की समस्या होती है। Haskell की “architectural asymptotics” काफी फायदेमंद हैं
मुझे लगता है GHC में SIMD support है या आ रहा है, लेकिन performance evaluate करते समय मैं उसी पर focus नहीं करूँगा
Haskell में matrix multiplication algorithm खुद नहीं लिखूँगा, लेकिन अगर speed को लेकर serious हूँ तो Rust या C में भी खुद नहीं लिखूँगा
numeric computing को अक्सर performance metric माना जाता है, लेकिन असल में बहुत कम लोगों का bottleneck वहीं होता है; और अगर bottleneck वहीं है, तो आप कौन-सी high-level language इस्तेमाल कर रहे हैं, यह ज्यादा मायने नहीं रखता
इस style की performance आम तौर पर CLI या web backend tasks के लिए पर्याप्त होती है। काफी तेज़ low-level code लिखने के tools भी हैं, लेकिन वे थोड़े rough हैं, इसलिए अगर आप सिर्फ वही करना चाहते हैं तो शायद यह best tool नहीं है
हालांकि जब optimize करने के लिए कुछ concentrated hotspots हों, तो यह काफी ठीक है
CPU profiling tools अच्छे हैं, इसलिए CPU hotspots ढूँढना और optimize करना अपेक्षाकृत pleasant है। दूसरी ओर lazy evaluation की वजह से ज्यादा आसानी से बनने वाले अजीब memory leaks को trace करना बहुत frustrating हो सकता है
benchmarks game के results देखें तो सबसे तेज़ Haskell implementations आम तौर पर सबसे तेज़ C versions से 2–5 गुना धीमे हैं, और काफी imperative style में लिखे गए हैं
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
“Creating prefix-free codes” section की table में typo लगता है। D
0110नहीं,0010होना चाहिए0110ambiguous कैसे नहीं हो सकता, अब समझ आयाimage में महिला की shirt पर क्या है, यह जानना चाहता हूँ
direct link: https://lazamar.github.io/images/data-compressor.svg