240 तत्वों का रहस्य: आपका Rust कोड अचानक धीमा क्यों हो जाता है?
\n\nएक स्थिति की कल्पना करें: आप Rust में उच्च-प्रदर्शन कोड लिख रहे हैं, सब कुछ तेज़ चल रहा है। आप 200 तत्वों वाले ऐरे पर एल्गोरिदम का परीक्षण करते हैं — बढ़िया। आप 40 और तत्व जोड़ते हैं (कुल 240) — और प्रदर्शन 2-3 गुना गिर जाता है। जादू जैसा लगता है? नहीं, यह भौतिकी है। अधिक सटीक रूप से — आधुनिक प्रोसेसर की आर्किटेक्चर।
\n\nStack Overflow पर प्रश्न "Why is there a large performance impact when looping over an array with 240 or more elements?" ने हज़ारों व्यूज़ प्राप्त किए और एक क्लासिक बन गया। 2025 में, नई माइक्रोआर्किटेक्चर और जटिल कैश पदानुक्रम के साथ, इस समस्या को समझना किसी भी Rust डेवलपर के लिए महत्वपूर्ण है।
\n\nइस लेख में हम केवल उत्तर को दोबारा नहीं बताएंगे। हम दिखाएंगे कि यह Rust में कैसा दिखता है, समस्या का निदान कैसे करें और, सबसे महत्वपूर्ण, — 2025 की आधुनिक प्रथाओं का उपयोग करके इससे कैसे बचें।
\n\nसमस्या की जड़: कैश मिस (Cache Misses)
\n\nप्रोसेसर सीधे RAM के साथ काम नहीं करता। यह कैश का उपयोग करता है — चिप पर अल्ट्रा-फास्ट मेमोरी। डेटा को cache lines (आमतौर पर 64 बाइट्स) नामक ब्लॉक में कैश में लोड किया जाता है।
\n\nसंख्या 240 का जादू L1 कैश के आकार (आमतौर पर डेटा के लिए 32 KB) और तत्व के आकार से संबंधित है। यदि ऐरे i32 (4 बाइट्स) है, तो 240 तत्व 960 बाइट्स लेते हैं। ऐसा लगता है कि यह L1 में फिट बैठता है। लेकिन समस्या यह है कि एल्गोरिदम एक साथ दो या तीन ऐरे के साथ काम कर सकता है, या कंपाइलर कोड को प्रभावी ढंग से वेक्टराइज़ नहीं कर पाता, जिससे कैश मिस (cache misses) होते हैं।
C++ में मूल प्रश्न में, सीमा ठीक 240 तत्व थी क्योंकि यह प्रीफ़ेचिंग (prefetching) और L1/L2 कैश आकार की विशेषताओं से टकराती थी।
\n\nRust में यह कैसा दिखता है?
\n\nआइए स्थिति का अनुकरण करें। हम ऐरे के तत्वों का योग करेंगे, लेकिन एक "स्टेप" के साथ जो कैश को तोड़ता है।
\n\nuse std::time::Instant;\n\nfn main() {\n // 240 तत्वों का ऐरे\n let size = 240;\n let data: Vec<i32> = (0..size).collect();\n \n // कोल्ड स्टार्ट\n let start = Instant::now();\n let sum: i32 = data.iter().sum();\n let duration = start.elapsed();\n println!("240 तत्व (अनुक्रमिक): {:?}", duration);\n \n // अब "खराब" एक्सेस 64 (cache line आकार) के स्टेप के साथ करते हैं\n let mut sum2 = 0i32;\n let start = Instant::now();\n for i in (0..size).step_by(64) {\n sum2 += data[i];\n }\n let duration2 = start.elapsed();\n println!("240 तत्व (स्टेप 64): {:?}", duration2);\n}\n\n\nआपको आश्चर्य होगा, लेकिन अंतर 10 गुना हो सकता है! पहला लूप अनुक्रमिक एक्सेस का उपयोग करता है — प्रोसेसर पूर्वानुमान लगाता है और पहले से cache lines लोड करता है। दूसरा — बड़े स्टेप के साथ रैंडम एक्सेस, जो कैश से बाहर निकाल देता है।
\n\nठीक 240 क्यों? गहरा कारण
\n\n2025 में, प्रोसेसर में 32 KB से 64 KB तक का L1 डेटा कैश होता है। लेकिन समस्या केवल आकार की नहीं है। यह कैश की साहचर्यता (set-associative cache) के बारे में है।
\n\nकैश को 64 अलमारियों (sets) वाली लाइब्रेरी के रूप में सोचें। प्रत्येक अलमारी में 8 किताबें (ways) हो सकती हैं। जब प्रोसेसर मेमोरी तक पहुँचता है, तो वह पते के आधार पर "अलमारी संख्या" की गणना करता है। यदि अलमारी में पहले से 8 किताबें हैं, तो एक को बाहर फेंकना होगा।
\n\nउन ऐरे के लिए जिनका आकार दो की घात का गुणज है (जैसे, 256), सभी तत्व एक ही "अलमारी" पर पड़ सकते हैं, जिससे cache conflict misses होते हैं। 240, 256 माइनस 16 है, जो इस गुणजता को तोड़ता है और... विरोधाभासी रूप से प्रीफ़ेचिंग की विशेषताओं के कारण स्थिति को और खराब कर देता है।
\n\nRust में व्यावहारिक परीक्षण
\n\nuse std::time::Instant;\n\nfn bench_access(size: usize) {\n let data: Vec<i32> = (0..size as i32).collect();\n \n // "खराब" पैटर्न: हर 16वें तत्व तक पहुँच\n let mut sum = 0i32;\n let start = Instant::now();\n for i in (0..size).step_by(16)