Static Search Trees : बाइनरी सर्च से 40 गुना तेज
(curiouscoding.nl)- "S+ ट्री" नाम के एक static search tree को implement करके sorted data पर high-speed search किया जाता है
- Algorithmica पोस्ट में प्रस्तावित code को शुरुआती आधार बनाकर optimize किया गया, और सुझाए गए अतिरिक्त ideas व improvements को code में बदला गया
- Assembly code का analysis करने के बाद संभव सभी instructions को optimize कर performance को अधिकतम किया गया
- कई queries को parallel में process करके throughput बढ़ाने के लिए batching जोड़ी गई
- लक्ष्य यह है कि S+ tree के जरिए sorted data पर high throughput बनाए रखते हुए queries को efficiently execute किया जाए
1. परिचय और प्रेरणा
-
समस्या की परिभाषा:
- इनपुट: sorted 32-bit unsigned integer list
vals: Vec<u32> - आउटपुट: न्यूनतम आकार वाला ऐसा data structure जो किसी query (q) से बड़ा या उसके बराबर मान लौटाए
- metric: प्रति सेकंड process की जा सकने वाली independent queries की संख्या
- इनपुट: sorted 32-bit unsigned integer list
-
उद्देश्य:
- bioinformatics में efficient data structure बनाना, खासकर DNA sequence indexing के लिए suffix array search की speed बढ़ाना
- CPU performance और SIMD instructions का उपयोग करके performance को अधिकतम करना
-
अनुशंसित सामग्री:
- binary search और array layout पर शोध (“Array Layouts for Comparison-Based Searching”)
- S+ tree और static B-tree का परिचय
2. S+ tree और optimization
2.1 binary search और Eytzinger layout
- Rust standard library की binary search cache-efficient नहीं है, और data size बढ़ने पर performance गिरती है
- Eytzinger layout:
- binary search tree को array form में store करके cache utilization को अधिकतम करता है
- performance: L3 cache से बड़े data पर binary search से अधिकतम 4 गुना तेज
2.2 S+ tree की अवधारणा
- S-tree:
- प्रति node 15 sorted values वाला hexadecimal tree form
- B-tree से अधिक efficient और Eytzinger layout से अधिक compact
- S+ tree:
- सभी data को leaf nodes में store करता है, और upper nodes में duplicate रूप से रखता है
- सरल search और efficient structure प्रदान करता है
2.3 find function optimization
- मूल linear search:
- data को traverse करके condition से मेल खाने वाला value लौटाता है (inefficient)
- automatic vectorization:
- branchless code में बदलकर SIMD instructions से performance 2 गुना बेहतर
- manual SIMD implementation:
- SIMD instructions का manually उपयोग कर optimization, 5 instructions के साथ performance को अधिकतम किया गया
3. batching और prefetching
3.1 batching
- कई queries को parallel में process करके memory latency को offset किया जाता है
- batch size बढ़ाने पर throughput बेहतर हुआ, और अधिकतम batch size 16 पर saturation आया
3.2 prefetching
- अगले node को पहले से memory में लाकर L3 cache से बड़े data पर performance सुधरती है
- batching के साथ मिलाकर processing time 45ns/query से 30ns/query तक घटा
4. data layout और structure optimization
4.1 node size बदलना
- प्रति node values को 15 तक घटाकर multiplication operation को सरल बनाया गया और cache efficiency बढ़ी
- L3 cache के अंदर के data पर थोड़ा performance improvement मिला
4.2 memory layout बदलना
- layout को reverse order में store करने या padding को कम करने वाली configurations का परीक्षण किया गया
- reverse और reduced-padding layouts दोनों का performance पर बड़ा असर नहीं पड़ा
5. data partitioning
5.1 prefix-based partitioning
- data के upper bits के आधार पर parts अलग किए गए
- छोटे data पर performance बेहतर हुई, लेकिन memory overhead आया
5.2 compressed subtrees
- हर subtree को pack करके space efficiency बढ़ाई गई
- parts के size को track करना पड़ता है, और query speed थोड़ी घटती है
6. multi-thread तुलना
- 6 threads उपयोग करने पर query time 27ns → 7ns तक घटा
- RAM bandwidth limit bottleneck बन गई
7. निष्कर्ष और आगे का शोध
- binary search की तुलना में 40 गुना से अधिक performance improvement (1150ns/query → 27ns/query)
- आगे के कार्य:
- data balancing optimization और RAM access कम करना
- range queries और sorted queries को handle करना
- suffix array search integration
2 टिप्पणियां
वाह... अगर इसे विभिन्न भाषाओं की built-in libraries में लागू किया जाए, तो इसका प्रभाव काफ़ी बड़ा हो सकता है।
Hacker News टिप्पणियाँ
देखा कि एल्गोरिथ्म से जुड़े low-level कंटेंट में Rust का इस्तेमाल धीरे-धीरे बढ़ रहा है। पहले ऐसे कंटेंट ज़्यादातर C(++) या वैज्ञानिक pseudocode में लिखे जाते थे। Rust का बढ़ता उपयोग सकारात्मक लगता है
queries को batch में बाँटने के तरीके की खोज नहीं की गई। cache के बाहर की table में lookup करना मुख्य लागत है
int32 values की संख्या बहुत बड़ी नहीं है, और पूरा bitset 512MB है। सामान्य data structure के रूप में Roaring Bitmaps की सिफारिश है
Rust में low-level efficiency बढ़ाने के तरीकों को देखकर हैरानी हुई। इसे C++ implementation से तुलना करके देखना चाहूँगा
Eytzinger tree का फ़ायदा यह है कि node index को inorder traversal position in बदलने का एक formula होता है
4GB memory में u32 खोजने पर ~27ns overhead आना आश्चर्यजनक है
Rust और C++ पर बहुत चर्चा है, लेकिन यह सोच रहा हूँ कि Common Lisp में SIMD बनाए रखते हुए इसे कैसे implement किया जाए
low-level optimization पर लेख पढ़ते समय हर बार यह महसूस होता है कि लेखक ने nanoseconds बचाने के लिए समय लगाया, उसके लिए आभार
लगता है कि 1.7 के figure 3 में त्रुटि है। 1.3 के figure 1 में y-axis label को "inversion throughput" की जगह सही किया जाना चाहिए, ऐसा सुझाव है
अगर कभी-कभी writes को support करना हो, तो एक बड़ा static search tree और एक छोटा writable tree इस्तेमाल किया जा सकता है