परिचय: छोटी दुनिया में बड़ी संख्याएँ
\n\nकल्पना करें: आप एक क्रिप्टोग्राफ़िक एल्गोरिदम लिख रहे हैं, भौतिक प्रक्रियाओं का अनुकरण कर रहे हैं, या बस नैनोमीटर में निकटतम आकाशगंगा की दूरी की गणना कर रहे हैं। और अचानक आपको एहसास होता है — मानक i64 (-9 क्विंटिलियन से 9 क्विंटिलियन तक) आपके लिए बेहद अपर्याप्त है। Rust में एक जीवन रक्षक है — i128 प्रकार। लेकिन यह एक प्रोसेसर पर कैसे काम करता है जो शारीरिक रूप से 128-बिट संख्याओं के साथ काम नहीं कर सकता? क्या यह जादू है?
स्पॉयलर: कोई जादू नहीं है, शानदार इंजीनियरिंग है। इस लेख में, हम यह समझेंगे कि कैसे Rust (या अधिक सटीक रूप से, हुड के नीचे LLVM) 64-बिट आर्किटेक्चर पर i128 का अनुकरण करता है। आप जानेंगे कि यह उतना धीमा क्यों नहीं है जितना लगता है, और इन विशाल संख्याओं के साथ कुशल कोड कैसे लिखें। यह शुष्क सिद्धांत नहीं है — हम प्रश्न से लेकर उदाहरणों पर वास्तविक अनुप्रयोग तक का सफर तय करेंगे।
1. हार्डवेयर वास्तविकता: 128 बिट एक चुनौती क्यों है
\n\nआधुनिक 64-बिट प्रोसेसर (x86-64, ARM64) में 64 बिट आकार के सामान्य प्रयोजन वाले रजिस्टर होते हैं। यह उनका "मूल" शब्द आकार है। 128-बिट संख्या के साथ काम करने के लिए, प्रोसेसर को दो 64-बिट हिस्सों पर कई ऑपरेशन करने होंगे: निचला (low) और ऊपरी (high)।
\n\nRust में, i128 प्रकार (और इसका अहस्ताक्षरित भाई u128) एक हार्डवेयर प्रकार नहीं है, बल्कि एक एग्रीगेट प्रकार है। इसे मेमोरी में दो आसन्न 64-बिट शब्दों के रूप में संग्रहीत किया जाता है। कंपाइलर (LLVM के माध्यम से) कोड उत्पन्न करता है जो इन शब्दों को एक इकाई के रूप में संसाधित करता है।
एक सरल उदाहरण:
\n\nfn main() {\n let big: i128 = 123456789012345678901234567890;\n println!("{}", big);\n}\n\nअसेंबली स्तर पर, यह दो 64-बिट रजिस्टरों (जैसे, rax और rdx) के साथ काम करने वाले निर्देशों के अनुक्रम में बदल जाएगा। LLVM ऐसे ऑपरेशनों को बहुत कुशलता से "जोड़ना" जानता है।
2. जोड़ की शारीरिक रचना: Rust i128 को कैसे जोड़ता है
\n\nआइए सबसे बुनियादी ऑपरेशन देखें — दो i128 को जोड़ना। आइए एक सरल फ़ंक्शन लिखें और cargo-show-asm या Compiler Explorer की मदद से हुड के नीचे देखें।
fn add_big(a: i128, b: i128) -> i128 {\n a + b\n}\n\nइसे x86-64 के लिए संकलित करें (बिना अनुकूलन के, ताकि यह अधिक स्पष्ट हो)। LLVM लगभग इस तरह एक स्यूडोकोड उत्पन्न करेगा:
\n\n- \n
aके निचले 64 बिट को रजिस्टरraxमें लोड करें, ऊपरी कोrdxमें। \n bके निचले 64 बिट कोrbxमें लोड करें, ऊपरी कोrcxमें। \n - निचले भागों को जोड़ें:
add rax, rbx— परिणामraxमें, कैरी फ्लैग (CF) सेट होता है यदि अतिप्रवाह होता है। \n - कैरी के साथ ऊपरी भागों को जोड़ें:
adc rdx, rcx—adc(add with carry) 1 जोड़ता है यदि CF फ्लैग सेट था। \n - परिणाम: निचले 64 बिट
raxमें, ऊपरीrdxमें। \n
यही अनुकरण है। एक के बजाय दो निर्देश। गुणा और भाग के लिए कोड अधिक जटिल होगा, लेकिन सिद्धांत वही है — 64-बिट ऑपरेशनों में विभाजन।
\n\n3. प्रदर्शन के बारे में क्या? मिथक और वास्तविकता
\n\nबहुत से लोग सोचते हैं कि i128 i64 से दोगुना धीमा है। यह पूरी तरह सच नहीं है। हाँ, प्रत्येक ऑपरेशन एक के बजाय 2-3 निर्देशों में बदल जाता है। लेकिन आधुनिक प्रोसेसर में होते हैं:
- \n
- पाइपलाइन और सुपरस्केलरिटी — प्रति क्लॉक कई निर्देश निष्पादित कर सकते हैं। \n
- शाखा भविष्यवाणी — तुलनाओं के लिए विशेष रूप से महत्वपूर्ण। \n
- बड़े कैश — मेमोरी के साथ काम प्रभावित नहीं होता, क्योंकि
i128केवल 16 बाइट लेता है। \n
व्यावहारिक परीक्षण (Rust, लूप में सरल अंकगणित):
\n\nuse std::time::Instant;\n\nfn main() {\n let n = 100_000_000;\n \n // i64\n let start = Instant::now();\n let m