64-बिट सिस्टम पर रस्ट का 128-बिट पूर्णांक `i128` कैसे काम करता है?


128

जंग में 128-बिट पूर्णांक होते हैं, इन्हें डेटा प्रकार i128(और u128अहस्ताक्षरित ints) के साथ चिह्नित किया जाता है :

let a: i128 = 170141183460469231731687303715884105727;

रस्ट i12864-बिट सिस्टम पर इन मूल्यों को कैसे काम करता है; जैसे कि यह इन पर अंकगणित कैसे करता है?

चूंकि, जहां तक ​​मुझे पता है, x86-64 सीपीयू के एक रजिस्टर में मूल्य फिट नहीं हो सकता है, क्या कंपाइलर किसी भी तरह एक i128मूल्य के लिए 2 रजिस्टरों का उपयोग करता है ? या वे बजाय प्रतिनिधित्व करने के लिए किसी तरह के बड़े पूर्णांक संरचना का उपयोग कर रहे हैं?



54
जब आपके पास केवल 10 उंगलियां होती हैं तो दो अंकों का पूर्णांक कैसे काम करता है?
जोर्ग डब्ल्यू मित्तग

27
@ जोर्जवामटैग: आह - केवल दस उंगलियों वाले पुराने "दो अंकों की संख्या"। हे हे। सोचा था कि आप मुझे उस पुराने के साथ मूर्ख बना सकते हैं, एह? ठीक है, मेरे दोस्त, जैसा कि कोई दूसरा-ग्रेडर आपको बता सकता है - कि पैर की उंगलियों के लिए क्या है! ( पीटर सेलर्स के लिए अपमानजनक माफी के साथ ... और लेडी लिटन :-)
बॉब जार्विस -

1
एफडब्ल्यूआईडब्ल्यू की अधिकांश एक्स 86 मशीनों में SIMD संचालन के लिए कुछ विशेष 128-बिट या बड़े रजिस्टर हैं। En.wikipedia.org/wiki/Streaming_SIMD_Extensions संपादित करें देखें : मैंने किसी तरह @ eckes की टिप्पणी को याद किया
Ryan1729

4
@ JörgWMittag नाह, कंप्यूटर वैज्ञानिक व्यक्तिगत उंगलियों को कम या विस्तार करके बाइनरी में गिनती करते हैं। और अब, 132 y'all, मैं घर जा रहा हूं;
--D

जवाबों:


141

सभी रस्ट के पूर्णांक प्रकार LLVM पूर्णांकों के लिए संकलित किए जाते हैं । LLVM अमूर्त मशीन 1 से 2 ^ 23 तक किसी भी बिट चौड़ाई के पूर्णांक की अनुमति देता है - 1. * LLVM निर्देश आमतौर पर किसी भी आकार के पूर्णांक पर काम करते हैं।

जाहिर है, वहाँ कई 8388607-बिट आर्किटेक्चर नहीं हैं, इसलिए जब कोड को देशी मशीन कोड में संकलित किया जाता है, तो एलएलवीएम को यह तय करना होगा कि इसे कैसे लागू किया जाए। addएलएलवीएम द्वारा ही एक सार निर्देश के शब्दार्थ को परिभाषित किया गया है। आमतौर पर, मूल कोड में एक एकल-निर्देश समतुल्य रखने वाले अमूर्त निर्देश को उस मूल अनुदेश के लिए संकलित किया जाएगा, जबकि जिन लोगों का अनुकरण नहीं किया जाएगा, संभवतः कई मूल निर्देशों के साथ। mcarton का उत्तर दर्शाता है कि LLVM देशी और उत्सर्जित निर्देशों को कैसे संकलित करता है।

(यह केवल उन पूर्णांकों पर लागू नहीं होता है जो मूल मशीन की तुलना में बड़े होते हैं जो समर्थन कर सकते हैं, लेकिन यह भी कि छोटे हैं। उदाहरण के लिए, आधुनिक आर्किटेक्चर मूल 8-बिट अंकगणितीय का समर्थन नहीं कर सकते हैं, इसलिए addदो i8एस पर एक अनुदेश का अनुकरण किया जा सकता है। एक व्यापक निर्देश के साथ, अतिरिक्त बिट्स को छोड़ दिया गया।)

क्या कंपाइलर किसी तरह एक i128मूल्य के लिए 2 रजिस्टरों का उपयोग करता है ? या वे किसी तरह के बड़े पूर्णांक संरचना का उपयोग कर रहे हैं उनका प्रतिनिधित्व करने के लिए?

एलएलवीएम आईआर के स्तर पर, इसका उत्तर न तो है: i128एक ही रजिस्टर में फिट बैठता है, हर दूसरे एकल-मूल्यवान प्रकार की तरह । दूसरी ओर, एक बार मशीन कोड में अनुवादित होने के बाद, वास्तव में दोनों के बीच कोई अंतर नहीं है, क्योंकि पूर्णांक की तरह ही संरचनाएं रजिस्टरों में विघटित हो सकती हैं। हालांकि अंकगणित करते समय, यह एक बहुत ही सुरक्षित शर्त है कि एलएलवीएम पूरी चीज को केवल दो रजिस्टरों में लोड करेगा।


* हालांकि, सभी एलएलवीएम बैकएंड समान नहीं बनाए गए हैं। यह उत्तर x86-64 से संबंधित है। मैं समझता हूं कि 128 से बड़ा आकार और दो की गैर-शक्तियों के लिए बैकएंड समर्थन धब्बेदार है (जो आंशिक रूप से समझा सकता है कि जंग केवल 8-, 16-, 32-, 64- और 128-बिट पूर्णांकों को उजागर करती है)। Reddit पर est31 के अनुसार , एक बैकएंड को लक्षित करते समय सॉफ्टवेयर में 128 बिट पूर्णांक को रस्टेक लागू करता है जो उन्हें मूल रूप से समर्थन नहीं करता है।


1
हुह, मुझे आश्चर्य है कि यह अधिक विशिष्ट 2 ^ 32 के बजाय 2 ^ 23 क्यों है (ठीक है, मोटे तौर पर बोलते हुए कि वे संख्या कितनी बार दिखाई देती है, संकलक बैकएंड द्वारा समर्थित पूर्णांकों की अधिकतम बिट चौड़ाई के संदर्भ में नहीं ...)
निधि मोनिका का मुकदमा

26
@NicHartley एलएलवीएम के कुछ आधारभूत क्षेत्रों में एक क्षेत्र है जहां उप-वर्ग डेटा संग्रहीत कर सकते हैं। Typeवर्ग के लिए इसका अर्थ है कि स्टोर करने के लिए 8 बिट्स हैं यह किस प्रकार का है (फ़ंक्शन, ब्लॉक, पूर्णांक, ...) और उप-वर्ग डेटा के लिए 24 बिट्स। IntegerTypeवर्ग तो उन 24 बिट का उपयोग करता आकार स्टोर करने के लिए, उदाहरणों बड़े करीने से 32 बिट में फिट करने के लिए अनुमति देता है!
टॉड सीवेल

56

कंपाइलर इन्हें कई रजिस्टरों में स्टोर करेगा और जरूरत पड़ने पर उन मूल्यों पर अंकगणित करने के लिए कई निर्देशों का उपयोग करेगा। अधिकांश ISAs में x86adc जैसा एक ऐड-ऑन-कैरी निर्देश है जो विस्तारित-सटीक पूर्णांक जोड़ने / उप करने के लिए इसे काफी कुशल बनाता है।

उदाहरण के लिए, दिया गया

fn main() {
    let a = 42u128;
    let b = a + 1337;
}

कंपाइलर x86-64 के लिए अनुकूलन के बिना संकलन करते समय निम्नलिखित उत्पन्न करता है:
(@PeterCordes द्वारा जोड़ी गई टिप्पणियाँ)

playground::main:
    sub rsp, 56
    mov qword ptr [rsp + 32], 0
    mov qword ptr [rsp + 24], 42         # store 128-bit 0:42 on the stack
                                         # little-endian = low half at lower address

    mov rax, qword ptr [rsp + 24]
    mov rcx, qword ptr [rsp + 32]        # reload it to registers

    add rax, 1337                        # add 1337 to the low half
    adc rcx, 0                           # propagate carry to the high half. 1337u128 >> 64 = 0

    setb    dl                           # save carry-out (setb is an alias for setc)
    mov rsi, rax
    test    dl, 1                        # check carry-out (to detect overflow)
    mov qword ptr [rsp + 16], rax        # store the low half result
    mov qword ptr [rsp + 8], rsi         # store another copy of the low half
    mov qword ptr [rsp], rcx             # store the high half
                             # These are temporary copies of the halves; probably the high half at lower address isn't intentional
    jne .LBB8_2                       # jump if 128-bit add overflowed (to another not-shown block of code after the ret, I think)

    mov rax, qword ptr [rsp + 16]
    mov qword ptr [rsp + 40], rax     # copy low half to RSP+40
    mov rcx, qword ptr [rsp]
    mov qword ptr [rsp + 48], rcx     # copy high half to RSP+48
                  # This is the actual b, in normal little-endian order, forming a u128 at RSP+40
    add rsp, 56
    ret                               # with retval in EAX/RAX = low half result

जहां आप देख सकते है कि मूल्य 42में संग्रहित है raxऔर rcx

(संपादक का नोट: x86-64 C कॉलिंग कन्वेंशन RDX में 128-बिट पूर्णांक लौटाता है: RAX। लेकिन यह mainबिल्कुल भी एक मान नहीं लौटाता है। सभी निरर्थक नकल पूरी तरह से अनुकूलन अक्षम करने से है, और यह कि Rust वास्तव में डीबग में ओवरफ्लो की जाँच करता है। मोड।)

तुलना के लिए, यहां x86-64 पर रस्ट 64-बिट पूर्णांकों के लिए एएसएम है, जहां किसी भी ऐड-ऑन-कैरी की आवश्यकता नहीं है, प्रत्येक मूल्य के लिए बस एक ही रजिस्टर या स्टैक-स्लॉट है।

playground::main:
    sub rsp, 24
    mov qword ptr [rsp + 8], 42           # store
    mov rax, qword ptr [rsp + 8]          # reload
    add rax, 1337                         # add
    setb    cl
    test    cl, 1                         # check for carry-out (overflow)
    mov qword ptr [rsp], rax              # store the result
    jne .LBB8_2                           # branch on non-zero carry-out

    mov rax, qword ptr [rsp]              # reload the result
    mov qword ptr [rsp + 16], rax         # and copy it (to b)
    add rsp, 24
    ret

.LBB8_2:
    call panic function because of integer overflow

सेटब / परीक्षण अभी भी पूरी तरह से निरर्थक है: jc(कूदो अगर CF = 1) ठीक काम करेगा।

ऑप्टिमाइज़ेशन सक्षम होने के साथ, रस्ट कंपाइलर ओवरफ्लो की जांच नहीं +करता है इसलिए जैसे काम करता है .wrapping_add()


4
@Anush नहीं, rax / rsp / ... 64-बिट रजिस्टर हैं। प्रत्येक 128-बिट संख्या को दो रजिस्टरों / मेमोरी स्थानों में संग्रहीत किया जाता है, जिसके परिणामस्वरूप दो 64-बिट जोड़ होते हैं।
मैनफैप

5
@Anush: नहीं, यह सिर्फ इतने सारे निर्देशों का उपयोग कर रहा है क्योंकि यह अनुकूलन अक्षमता के साथ संकलित है। आप देखना चाहते हैं ज्यादा सरल कोड (बस जोड़ें / एडीसी) की तरह यदि आप एक समारोह है कि दो ले लिया संकलित u128(इस तरह args और एक मान दिया godbolt.org/z/6JBza0 करने से रोकने के लिए संकलक बजाय अनुकूलन को अक्षम करने का), संकलन-समय-निरंतर आर्ग पर निरंतर-प्रसार।
पीटर कॉर्ड्स

3
@ CAD97 रिलीज़ मोड रैपिंग अंकगणित का उपयोग करता है, लेकिन डीबग मोड की तरह अतिप्रवाह और आतंक के लिए जाँच नहीं करता है। यह व्यवहार RFC 560 द्वारा परिभाषित किया गया था । यह यूबी नहीं है।
ट्रेंटक्ल

3
@PeterCordes: विशेष रूप से, Rust भाषा निर्दिष्ट करती है कि अतिप्रवाह अनिर्दिष्ट है, और rustc (एकमात्र कंपाइलर) दो व्यवहारों को चुनने के लिए निर्दिष्ट करता है: पैनिक या रैप। आदर्श रूप से, पैनिक का उपयोग डिफ़ॉल्ट रूप से किया जाएगा। व्यवहार में, उप-इष्टतम कोड-पीढ़ी के कारण, रिलीज़ मोड में डिफ़ॉल्ट रैप होता है, और मुख्यधारा के उपयोग के लिए कोड-जेनरेशन के लिए (यदि कभी भी) कोड-जनरेशन "अच्छा पर्याप्त" है, तो एक दीर्घकालिक लक्ष्य पैनिक में जाना है। इसके अलावा, सभी रुस्ट अभिन्न प्रकारों को एक व्यवहार चुनने के लिए संचालन नाम का समर्थन करते हैं: जाँच, लपेटकर, संतृप्त करना, ... इसलिए आप प्रति ऑपरेशन के आधार पर चयनित व्यवहार को ओवरराइड कर सकते हैं।
Matthieu M.

1
@ मैथ्यूएमएम .: हां, मैं रैपिंग बनाम चैक बनाम सैचुरेटिंग ऐड / सब / शिफ्ट / आदिम प्रकारों पर जो भी तरीके पसंद करता हूं, उसे पसंद करता हूं। C के रैपिंग के अहस्ताक्षरित की तुलना में बहुत बेहतर, UB ने आपको उस आधार पर चुनने के लिए मजबूर किया। वैसे भी, कुछ ISAs घबराहट के लिए कुशल समर्थन प्रदान कर सकते हैं, उदाहरण के लिए एक चिपचिपा झंडा जिसे आप संचालन के पूरे अनुक्रम के बाद देख सकते हैं। (X86 के ओएफ या सीएफ के विपरीत जो 0 या 1 के साथ ओवरराइट किए गए हैं।) जैसे कि एग्नर फॉग का प्रस्तावित फॉरवर्डकॉम आईएसए ( agner.org/optimize/blog/read.php?i=421#478 ) लेकिन फिर भी किसी भी गणना को करने के लिए कॉन्स्टिपिन ऑप्टिमाइज़ेशन अनुकूलन जंग स्रोत नहीं किया था। : /
पीटर कॉर्ड्स

30

हां, ठीक उसी तरह जिस तरह 32-बिट मशीनों पर 64-बिट पूर्णांकों को संभाला जाता था, या 16-बिट मशीनों पर 32-बिट पूर्णांक, या 8-बिट मशीनों पर 16- और 32-बिट पूर्णांक (अभी भी माइक्रोकंट्रोलर पर लागू होते हैं)! )। हां, आप संख्या को दो रजिस्टरों, या मेमोरी स्थानों, या जो कुछ भी (यह वास्तव में कोई फर्क नहीं पड़ता) में संग्रहीत करते हैं। जोड़ और घटाव तुच्छ होते हैं, दो निर्देश लेते हैं और कैरी फ्लैग का उपयोग करते हैं। गुणन के लिए तीन गुणा और कुछ परिवर्धन की आवश्यकता होती है (यह 64-बिट चिप्स के लिए पहले से ही 64x64-> 128 गुणा ऑपरेशन है जो दो रजिस्टरों के लिए आउटपुट के लिए सामान्य है)। डिवीजन ... एक सबरूटीन की आवश्यकता होती है और यह काफी धीमा होता है (कुछ मामलों को छोड़कर जहां एक स्थिर द्वारा विभाजन को एक शिफ्ट या एक गुणा में बदला जा सकता है), लेकिन यह अभी भी काम करता है। बिटवाइज़ और / या / एक्सआर को केवल ऊपर और नीचे के हिस्सों में अलग-अलग करना होता है। घुमावों को रोटेशन और मास्किंग के साथ पूरा किया जा सकता है। और यह बहुत ज्यादा चीजों को कवर करता है।


26

शायद x86_64 पर एक स्पष्ट उदाहरण प्रदान करने के लिए, -Oध्वज, फ़ंक्शन के साथ संकलित किया गया

pub fn leet(a : i128) -> i128 {
    a + 1337
}

के लिए संकलित करता है

example::leet:
  mov rdx, rsi
  mov rax, rdi
  add rax, 1337
  adc rdx, 0
  ret

(मेरे मूल पोस्ट के u128बजाय i128आपके बारे में पूछा गया था। फ़ंक्शन समान कोड को किसी भी तरह से संकलित करता है, एक अच्छा प्रदर्शन जो हस्ताक्षरित और अहस्ताक्षरित जोड़ आधुनिक सीपीयू पर समान है।)

अन्य लिस्टिंग ने अडॉप्टिमाइज्ड कोड का उत्पादन किया। डिबगर में कदम रखना सुरक्षित है, क्योंकि यह सुनिश्चित करता है कि आप कहीं भी एक ब्रेकपॉइंट लगा सकते हैं और कार्यक्रम की किसी भी रेखा पर किसी भी चर की स्थिति का निरीक्षण कर सकते हैं। यह धीमा और पढ़ने में कठिन है। अनुकूलित संस्करण कोड के बहुत करीब है जो वास्तव में उत्पादन में चलेगा।

aइस फ़ंक्शन का पैरामीटर 64-बिट रजिस्टर, आरएसआई: आरडीआई की एक जोड़ी में पारित किया गया है। परिणाम रजिस्टरों की एक और जोड़ी में वापस किया जाता है, rdx: rax। कोड की पहली दो पंक्तियाँ योग को आरंभ करती हैं a

तीसरी पंक्ति इनपुट के कम शब्द में 1337 जोड़ती है। यदि यह ओवरफ्लो होता है, तो यह सीपीयू के कैरी फ्लैग में 1 ले जाता है। चौथी पंक्ति इनपुट के उच्च शब्द में शून्य जोड़ती है - प्लस 1 अगर यह ले गया है।

आप इसे एक अंकों की संख्या के दो अंकों की संख्या के सरल जोड़ के रूप में सोच सकते हैं

  a  b
+ 0  7
______
 

लेकिन आधार में 18,446,744,073,709,551,616 हैं। आप अभी भी सबसे कम "अंक" जोड़ रहे हैं, संभवतः अगले कॉलम पर 1 ले जा रहे हैं, फिर अगले अंक और कैरी को जोड़ सकते हैं। घटाव बहुत समान है।

गुणन को पहचान (2⁶⁴a + b) (2 +c + d) = 2¹²⁸ac + 2 b (विज्ञापन + bc) + bd का उपयोग करना चाहिए, जहाँ इनमें से प्रत्येक गुणनफल उत्पाद के ऊपरी आधे भाग को एक रजिस्टर में और उत्पाद के निचले आधे हिस्से को लौटाता है एक और। उन में से कुछ पद छोड़ दिए जाएंगे, क्योंकि 128 वें से ऊपर बिट्स एक में फिट नहीं u128होते हैं और खारिज कर दिए जाते हैं। फिर भी, यह कई मशीन निर्देश लेता है। डिवीजन भी कई कदम उठाता है। एक हस्ताक्षरित मूल्य के लिए, गुणन और विभाजन को अतिरिक्त रूप से ऑपरेंड और परिणाम के संकेतों को बदलने की आवश्यकता होगी। वे ऑपरेशन बहुत कुशल नहीं हैं।

अन्य आर्किटेक्चर पर, यह आसान या कठिन हो जाता है। RISC-V एक 128-बिट इंस्ट्रक्शन-सेट एक्सटेंशन को परिभाषित करता है, हालांकि मेरी जानकारी के लिए किसी ने इसे सिलिकॉन में लागू नहीं किया है। इस विस्तार के बिना, RISC-V वास्तुकला मैनुअल एक सशर्त शाखा की सिफारिश करता है:addi t0, t1, +imm; blt t0, t1, overflow

SPARC में x86 के नियंत्रण झंडे की तरह नियंत्रण कोड होते हैं, लेकिन आपको add,ccउन्हें सेट करने के लिए , एक विशेष निर्देश का उपयोग करना होगा। दूसरी ओर, MIPS आपको यह जांचने की आवश्यकता है कि क्या दो अहस्ताक्षरित पूर्णांकों का योग किसी एक ऑपरेंड से कम है। यदि ऐसा है, तो इसके अलावा बह निकला। कम से कम आप सशर्त शाखा के बिना कैरी बिट के मूल्य में एक और रजिस्टर सेट करने में सक्षम हैं।


1
अंतिम पैराग्राफ: परिणाम के उच्च बिट को देखकर दो में से कौन सा अहस्ताक्षरित संख्या अधिक है sub, आपको बिट इनपुट के n+1लिए nथोड़ा उप परिणाम चाहिए । यानी आपको कैरी-आउट देखने की जरूरत है, न कि समान-चौड़ाई के परिणाम के संकेत बिट। यही कारण है कि x86 अहस्ताक्षरित शाखा की स्थिति सीएफ (पूर्ण तार्किक परिणाम के 64 या 32) पर आधारित हैं, एसएफ (बिट 63 या 31) नहीं।
पीटर कॉर्डेस

1
पुनः: divmod: AArch64 का दृष्टिकोण विभाजन प्रदान करना है और एक निर्देश है जो पूर्णांक करता है x - (a*b), शेष को लाभांश, भागफल और भाजक से गणना करता है । (यह विभाजन भाग के लिए गुणक व्युत्क्रम का उपयोग करते हुए निरंतर विभाजक के लिए भी उपयोगी है)। मैंने ISAs के बारे में नहीं पढ़ा था जो डिव + मॉड निर्देश को एक सिंगल डिमॉड ऑपरेशन में फ्यूज करता था; काफी अच्छा है।
पीटर कॉर्डेस

1
पुन: झंडे: हाँ, एक झंडे का उत्पादन एक दूसरा आउटपुट है जिसे OOO निष्पादित + रजिस्टर-रीनेमिंग किसी भी तरह से संभालना है। x86 CPU कुछ अतिरिक्त बिट्स को पूर्णांक परिणाम के साथ रखकर संभालते हैं, जो कि FLAGS मान पर आधारित होता है, इसलिए संभवतया जब ZF, SF, और PF मक्खी पर उत्पन्न होते हैं। मुझे लगता है कि इस बारे में एक इंटेल पेटेंट है। इसलिए यह उन आउटपुटों की संख्या को कम कर देता है जिन्हें अलग-अलग 1 पर वापस ट्रैक करना होता है। (इंटेल सीपीयू में, कोई भी यूओपी 1 से अधिक पूर्णांक रजिस्टर नहीं लिख सकता है; उदाहरण mul r642 यूओपी है, जिसमें दूसरा आरडीएक्स उच्च आधा लिखता है)।
पीटर कॉर्ड्स

1
लेकिन कुशल विस्तारित-परिशुद्धता के लिए, झंडे बहुत अच्छे हैं। सुपरस्क्लेयर-इन-ऑर्डर निष्पादन के लिए नाम बदलने के बिना मुख्य समस्या है । झंडे एक WAW खतरा हैं (लिखने के बाद लिखें)। बेशक, ऐड-ऑन-कैरी निर्देश 3-इनपुट हैं, और यह भी ट्रैक करने के लिए एक महत्वपूर्ण समस्या है। इंटेल से पहले Broadwell डीकोड adc, sbbऔर cmov2 UOPs प्रत्येक के लिए। (हैवेल ने एफएमए के लिए 3-इनपुट यूओपी की शुरुआत की, ब्रॉडवेल ने पूर्णांक तक बढ़ा दिया।)
पीटर कॉर्ड्स

1
RISC झंडे वाले आईएसए आमतौर पर फ्लैग-सेटिंग को वैकल्पिक बनाते हैं, जिसे एक अतिरिक्त बिट द्वारा नियंत्रित किया जाता है। जैसे ARM और SPARC इस तरह हैं। पॉवरपीसी हमेशा की तरह सब कुछ अधिक जटिल बनाता है: इसमें 8 कंडीशन-कोड रजिस्टर (सेव / रिस्टोर के लिए एक साथ 32-बिट रजिस्टर में एक साथ पैक किया जाता है) ताकि आप cc0 या cc7 या जो भी तुलना कर सकते हैं। और फिर और या या शर्त-कोड एक साथ! शाखा और cmov निर्देश चुन सकते हैं कि कौन से CR को पढ़ना है। तो यह आपको एक बार में उड़ान में कई झंडे डिप चेन रखने की क्षमता देता है, जैसे x86 ADCX / ADOX। alanclements.org/power%20pc.html
पीटर कॉर्ड्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.