assembly - क्या x86-64 ABI के लिए एक पॉइंटर में 32 बिट ऑफसेट जोड़ने पर एक साइन या शून्य एक्सटेंशन की आवश्यकता है?




compiler-optimization sign-extension (2)

सारांश: मैं अपनी ऑप्टिमाइज़ेशन का मार्गदर्शन करने के लिए असेंबली कोड देख रहा था और सूचक को int32 जोड़ते समय बहुत सारे साइन या शून्य एक्सटेंशन देख रहा था।

void Test(int *out, int offset)
{
    out[offset] = 1;
}
-------------------------------------
movslq  %esi, %rsi
movl    $1, (%rdi,%rsi,4)
ret

सबसे पहले, मैंने सोचा कि मेरे कंपाइलर को 32 बिट को 64 बिट के पूर्णांक में जोड़ने के लिए चुनौती दी गई थी, लेकिन मैंने इंटेल आईसीसी 11, आईसीसी 14 और जीसीसी 5.3 के साथ इस व्यवहार की पुष्टि की है।

यह thread मेरे निष्कर्षों की पुष्टि करता है, लेकिन यह स्पष्ट नहीं है कि संकेत या शून्य विस्तार आवश्यक है या नहीं। यह संकेत / शून्य एक्सटेंशन केवल तभी आवश्यक होगा जब ऊपरी 32 बिट्स पहले से सेट न हों। लेकिन क्या x86-64 ABI की आवश्यकता के लिए पर्याप्त स्मार्ट नहीं होगा?

मैं अपने सभी पॉइंटर ऑफ़सेट को ssize_t में बदलने के लिए अनिच्छुक हूँ क्योंकि रजिस्टर स्पिल करने से कोड के कैश फ़ुटप्रिंट में वृद्धि होगी।


EOF की टिप्पणी से संकेत मिलता है कि संकलक यह मान नहीं सकता है कि 32-बिट तर्क को पारित करने के लिए उपयोग किए जाने वाले 64-बिट रजिस्टर के ऊपरी 32 बिट्स का कोई विशेष मूल्य नहीं है। यह संकेत या शून्य विस्तार को आवश्यक बनाता है।

इसे रोकने का एकमात्र तरीका तर्क के लिए 64-बिट प्रकार का उपयोग करना होगा, लेकिन इससे कॉलर को मान बढ़ाने की आवश्यकता होती है, जिसमें सुधार नहीं हो सकता है। मैं रजिस्टर स्पिल के आकार के बारे में बहुत अधिक चिंता नहीं करूंगा, हालांकि, जिस तरह से आप इसे कर रहे हैं, अब यह संभवतः अधिक है कि विस्तार के बाद मूल मूल्य मृत हो जाएगा और यह 64-बिट विस्तारित मूल्य है जो स्पिल किया जाएगा । यहां तक ​​कि अगर यह मृत नहीं है, तो कंपाइलर 64-बिट मूल्य को फैलाना पसंद कर सकता है।

यदि आप वास्तव में अपनी मेमोरी फ़ुटप्रिंट के बारे में चिंतित हैं और आपको बड़े 64-बिट एड्रेस स्पेस की आवश्यकता नहीं है तो आप x32 ABI को देख सकते हैं जो ILP32 प्रकारों का उपयोग करता है लेकिन पूर्ण 64-बिट इंस्ट्रक्शन सेट का समर्थन करता है।


हां, आपको यह मानना ​​होगा कि एक arg या रिटर्न-वैल्यू रजिस्टर के उच्च 32 बिट्स में कचरा होता है। फ्लिप की तरफ, आपको उच्च कॉल में कचरा छोड़ने की इजाजत है जब आप कॉल या खुद लौटते हैं। यानी बोझ उच्च बिट्स को अनदेखा करने के लिए प्राप्त पक्ष पर है, उच्च बिट्स को साफ करने के लिए पासिंग साइड पर नहीं।

64-बिट प्रभावी पते में मान का उपयोग करने के लिए आपको 64 बिट्स पर हस्ताक्षर या शून्य करने की आवश्यकता है। X32 ABI में , gcc अक्सर एक निर्देश अनुक्रमणिका के रूप में उपयोग किए जाने वाले संभावित-ऋणात्मक पूर्णांक को संशोधित करने वाले प्रत्येक निर्देश के लिए 64-बिट ऑपरेंड-आकार का उपयोग करने के बजाय 32-बिट प्रभावी पते का उपयोग करता है।

मानक:

X86-64 SysV ABI केवल इस बारे में कुछ भी कहता है कि _Bool (उर्फ bool ) के लिए एक रजिस्टर के कौन से हिस्से _Bool । पृष्ठ 20:

जब प्रकार का मान _Bool वापस लौटाया जाता है या एक रजिस्टर में या स्टैक पर पारित किया जाता है, तो बिट 0 में सत्य मान होता है और बिट्स 1 से 7 शून्य होगा (फुटनोट 14: अन्य बिट्स अनिर्दिष्ट हैं, इसलिए उन मानों का उपभोक्ता पक्ष कर सकता है) इस पर भरोसा करते हुए 0 या 1 होने पर 8 बिट पर काट दिया जाता है)

इसके अलावा, एफपी रजिस्टर की संख्या रखने वाले %al बारे में सामान, वैरग कार्यों के लिए नहीं, पूरे %rax

X32 और x86-64 ABI दस्तावेज़ों के लिए github पृष्ठ पर इस सटीक प्रश्न के बारे में एक खुला github मुद्दा है

ABI आगे ​​कोई आवश्यकता या स्थान नहीं देता है, बल्कि पूर्णांक या वेक्टर रजिस्टर के उच्च भाग की सामग्री पर गारंटी देता है, जो कि args या रिटर्न मान रखता है, इसलिए कोई भी नहीं है। मुझे इस तथ्य की पुष्टि माइकल मैत्ज़ (एबीआई में से एक अनुरक्षक) से ईमेल के माध्यम से हुई है: "आम तौर पर, अगर एबीआई कुछ नहीं कहता है, तो आप इस पर भरोसा नहीं कर सकते।"

उन्होंने यह भी पुष्टि की है कि उदाहरण के लिए, addps > = 3.6 का एक addps का उपयोग जो उच्च तत्वों में कचरा के साथ अतिरिक्त एफपी अपवादों को धीमा या बढ़ा सकता है, एक बग है (जो मुझे याद दिलाता है कि मुझे इसकी रिपोर्ट करनी चाहिए)। वह कहते हैं कि यह एक बार एक glibc गणित फ़ंक्शन के एएमडी कार्यान्वयन के साथ एक मुद्दा था। स्केलर double या float आर्ग को पास करते समय सामान्य सी कोड वेक्टर रेज के उच्च तत्वों में कचरा छोड़ सकता है

वास्तविक व्यवहार जो मानक में प्रलेखित (अभी तक) नहीं है:

संकीर्ण फ़ंक्शन तर्क, यहां तक ​​कि _Bool / bool , 32 बिट्स पर हस्ताक्षर या शून्य-विस्तारित हैं। क्लैंग यहां तक ​​कि कोड बनाता है जो इस व्यवहार (2007 से, जाहिरा तौर पर) पर निर्भर करता है। ICC17 ऐसा नहीं करता है , इसलिए ICC और clang ABI- संगत नहीं हैं , यहां तक ​​कि C. के लिए भी clang- संकलित कार्यों को x86-64 SysV ABI के लिए ICC- संकलित कोड से नहीं कहते हैं, यदि पहले 6 में से कोई भी चार्जर नहीं आता है 32-बिट की तुलना में संकीर्ण हैं।

यह रिटर्न मानों पर लागू नहीं होता है, केवल args: gcc और clang दोनों यह मानते हैं कि रिटर्न-वैल्यू उन्हें प्राप्त होती है, जिसमें केवल टाइप की चौड़ाई तक मान्य डेटा होता है। उदाहरण के लिए, gcc ऐसे कार्यों को वापस कर देगा, जो उच्च स्तर के 24 %eax बिट्स में कचरा छोड़ते हैं।

एबीआई चर्चा समूह पर एक हालिया धागा 8 बिट्स और 32 बिट्स के लिए 16-बिट आर्गन के नियमों को स्पष्ट करने का एक प्रस्ताव था, और शायद इसकी आवश्यकता के लिए एबीआई को संशोधित करें। प्रमुख कंपाइलर (आईसीसी को छोड़कर) पहले से ही ऐसा करते हैं, लेकिन यह कॉलर्स और कैलिस के बीच अनुबंध में बदलाव होगा।

यहां एक उदाहरण दिया गया है (इसे अन्य संकलक के साथ देखें या गॉडबॉल्ट कंपाइलर एक्सप्लोरर पर कोड को ट्विक करें , जहां मैंने कई सरल उदाहरण शामिल किए हैं जो केवल पहेली के एक टुकड़े को प्रदर्शित करते हैं, साथ ही साथ यह बहुत कुछ प्रदर्शित करता है):

extern short fshort(short a);
extern unsigned fuint(unsigned int a);

extern unsigned short array_us[];
unsigned short lookupu(unsigned short a) {
  unsigned int a_int = a + 1234;
  a_int += fshort(a);                 // NOTE: not the same calls as the signed lookup
  return array_us[a + fuint(a_int)];
}

# clang-3.8 -O3  for x86-64.    arg in %rdi.  (Actually in %di, zero-extended to %edi by our caller)
lookupu(unsigned short):
    pushq   %rbx                      # save a call-preserved reg for out own use.  (Also aligns the stack for another call)
    movl    %edi, %ebx                # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx)
    movswl  %bx, %edi                 # sign-extend to call a function that takes signed short instead of unsigned short.
    callq   fshort(short)
    cwtl                              # Don't trust the upper bits of the return value.  (This is cdqe, Intel syntax.  eax = sign_extend(ax))
    leal    1234(%rbx,%rax), %edi     # this is the point where we'd get a wrong answer if our arg wasn't zero-extended.  gcc doesn't assume this, but clang does.
    callq   fuint(unsigned int)
    addl    %ebx, %eax                # zero-extends eax to 64bits
    movzwl  array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax
    popq    %rbx
    retq

नोट: movzwl array_us(,%rax,2) समतुल्य होगा, लेकिन कोई छोटा नहीं। अगर हम %rax के उच्च बिट्स पर fuint() रिटर्न वैल्यू में array_us(%rbx, %rax, 2) हो सकते हैं, तो कंपाइलर add इन्स का उपयोग करने के बजाय array_us(%rbx, %rax, 2) का उपयोग कर सकता है।

प्रदर्शन के निहितार्थ

उच्च 32 को अपरिभाषित छोड़कर जानबूझकर किया गया है, और मुझे लगता है कि यह एक अच्छा डिजाइन निर्णय है।

32-बिट ऑप्स करते समय उच्च 32 को अनदेखा करना नि: शुल्क है। एक 32-बिट ऑपरेशन शून्य इसके परिणाम को 64-बिट में मुफ्त में फैलाता है , इसलिए आपको केवल एक अतिरिक्त mov edx, edi या कुछ और चाहिए, यदि आप 64-बिट एड्रेसिंग मोड या 64-बिट ऑपरेशन में सीधे reg का उपयोग कर सकते थे।

कुछ फ़ंक्शंस किसी भी इन्सान को उनके आर्गन्स को पहले से ही 64-बिट तक बढ़ाने से नहीं बचाएंगे, इसलिए यह कॉल करने वालों के लिए हमेशा ऐसा करने के लिए एक संभावित बेकार है। कुछ फ़ंक्शंस एक तरह से अपने आर्गन्स का उपयोग करते हैं, जिन्हें आर्ग की हस्ताक्षर से विपरीत विस्तार की आवश्यकता होती है, इसलिए इसे कैलली तक छोड़ने के लिए यह तय करना है कि क्या अच्छा करना है।

शून्य-विस्तार 64-बिट की परवाह किए बिना हस्ताक्षर के अधिकांश कॉलर्स के लिए मुफ्त होगा, हालांकि, और एक अच्छा विकल्प हो सकता है एबीआई डिजाइन विकल्प। चूंकि arg regs को वैसे भी बंद कर दिया जाता है, इसलिए कॉल करने वाले को पहले से ही कुछ अतिरिक्त करने की आवश्यकता होती है अगर वह कॉल के दौरान पूरे 64-बिट मान को रखना चाहता है, जहां यह केवल कम 32 पास करता है। इस प्रकार यह आमतौर पर अतिरिक्त खर्च होता है जब आपको 64-बिट की आवश्यकता होती है कॉल से पहले कुछ के लिए परिणाम, और फिर एक समारोह में एक छोटा संस्करण पास करें। X86-64 SysV में, आप RDI में अपना परिणाम उत्पन्न कर सकते हैं और इसका उपयोग कर सकते हैं, और फिर call foo जो केवल EDI के साथ दिखेगा।

16-बिट और 8-बिट ऑपरेंड-आकार अक्सर झूठी निर्भरता (एएमडी, पी 4, या सिल्वरमोंट, और बाद में एसएनबी-परिवार), या आंशिक-रजिस्टर स्टाल (पूर्व एसएनबी) या मामूली मंदी (सैंडब्रिज) की ओर ले जाते हैं, इसलिए अनिर्दिष्ट व्यवहार आरजी-पासिंग के लिए 8 बी और 16 बी प्रकार को 32 बी तक बढ़ाया जाना आवश्यक है। देखें कि जीसीसी आंशिक रजिस्टर का उपयोग क्यों नहीं करता है? उन माइक्रोआर्किटेक्चर पर अधिक जानकारी के लिए।

यह शायद वास्तविक कोड में कोड-आकार के लिए एक बड़ा सौदा नहीं है, क्योंकि छोटे कार्य हैं / static inline होना चाहिए, और आर्ग-हैंडलिंग इंसन्स बड़े कार्यों का एक छोटा हिस्सा हैं । अंतर-प्रक्रियात्मक अनुकूलन कॉल के बीच ओवरहेड को हटा सकता है जब कंपाइलर दोनों परिभाषाओं को देख सकता है, यहां तक ​​कि बिना इनलाइन किए भी। (आईडीके व्यवहार में कितनी अच्छी तरह संकलक करते हैं।)

मुझे यकीन नहीं है कि uintptr_t का उपयोग करने के लिए फ़ंक्शन हस्ताक्षर बदलने से 64-बिट पॉइंटर्स के साथ समग्र प्रदर्शन में मदद मिलेगी या चोट लगी होगी। मुझे स्केलर के लिए स्टैक स्पेस की चिंता नहीं होगी। अधिकांश फ़ंक्शन में, कंपाइलर अपने स्वयं के चर को रजिस्टरों में रखने के लिए पर्याप्त कॉल-संरक्षित रजिस्टरों (जैसे %rbx और %rbp ) को %rbp देता है। 4B के बजाय 8B फैल के लिए एक छोटा सा अतिरिक्त स्थान नगण्य है।

जहां तक ​​कोड-आकार, 64-बिट मानों के साथ काम करने के लिए कुछ इंसक्स पर REX उपसर्ग की आवश्यकता होती है जो अन्यथा आवश्यक नहीं होते। शून्य से लेकर 64-बिट तक मुफ्त में होता है अगर किसी ऐरे इंडेक्स के रूप में उपयोग होने से पहले 32-बिट वैल्यू पर किसी भी ऑपरेशन की आवश्यकता होती है। यदि आवश्यक हो तो साइन-एक्सटेंशन हमेशा एक अतिरिक्त निर्देश लेता है। लेकिन संकलक निर्देश को बढ़ाने और उसके साथ काम करने के लिए 64-बिट हस्ताक्षरित मूल्य के रूप में शुरू से निर्देश बचाने के लिए, अधिक आरईएक्स उपसर्गों की आवश्यकता पर कर सकता है। (हस्ताक्षरित अतिप्रवाह यूबी है, जिसे चारों ओर लपेटने के लिए परिभाषित नहीं किया गया है, इसलिए संकलक अक्सर एक आईएनट के साथ लूप के अंदर साइन-एक्सटेंशन को फिर से करने से बच सकते हैं जो कि int i arr[i] करता arr[i]

आधुनिक सीपीयू आमतौर पर insn size की तुलना में insn count के बारे में अधिक ध्यान रखते हैं। हॉट कोड अक्सर सीपीयू में यूओपी कैश से चलता है जो उनके पास होता है। फिर भी, छोटा कोड यूओपी कैश में घनत्व में सुधार कर सकता है। यदि आप अधिक या धीमे इन्सानों का उपयोग किए बिना कोड आकार को बचा सकते हैं, तो यह एक जीत है, लेकिन आमतौर पर इसके लिए कुछ और त्यागने के लायक नहीं है जब तक कि यह कोड आकार का एक बहुत कुछ नहीं है।

जैसे कि एक अतिरिक्त LEA निर्देश [reg + disp8] को अनुमति देने के लिए, disp32 बजाय एक दर्जन बाद के निर्देशों के लिए संबोधित disp32 । या mov [rdi+n], 0 से पहले कई mov [rdi+n], 0 निर्देश स्रोत के साथ इम 32 = 0 को बदलने के लिए निर्देश। (खासकर यदि वह सूक्ष्म संलयन की अनुमति देता है जहां यह आरआईपी-सापेक्ष + तत्काल के साथ संभव नहीं होगा, क्योंकि वास्तव में जो मायने रखता है वह फ्रंट-एंड यूओपी काउंट है, न कि निर्देश गणना।)







sign-extension