संकलनकर्ता यहां एक कैली-सेव किए गए रजिस्टर का उपयोग करने पर जोर क्यों देते हैं?


10

इस C कोड पर विचार करें:

void foo(void);

long bar(long x) {
    foo();
    return x;
}

जब मैं इसे -O3या तो GCC 9.3 पर संकलित करता हूं या -Os, मुझे यह मिलता है:

bar:
        push    r12
        mov     r12, rdi
        call    foo
        mov     rax, r12
        pop     r12
        ret

क्लेग से आउटपुट कैली-सेव रजिस्टर के rbxबजाय चुनने के अलावा समान है r12

हालाँकि, मैं ऐसी विधानसभा देखना चाहता / चाहता हूँ जो इस तरह दिखे:

bar:
        push    rdi
        call    foo
        pop     rax
        ret

अंग्रेजी में, मैं यहां देख रहा हूं कि क्या हो रहा है:

  • स्टैक के लिए कैली-सेव किए गए रजिस्टर के पुराने मूल्य को पुश करें
  • xउस कैली-सेव्ड रजिस्टर में चले जाएं
  • कॉल foo
  • xकैली-सेव रजिस्टर से रिटर्न-वैल्यू रजिस्टर में ले जाएँ
  • कैली-सेव किए गए रजिस्टर के पुराने मूल्य को पुनर्स्थापित करने के लिए स्टैक को पॉप करें

क्यों एक शांत-बचाया रजिस्टर के साथ गड़बड़ करने के लिए परेशान? इसके बजाय ऐसा क्यों नहीं करते? यह छोटा, सरल, और शायद अधिक तेज़ लगता है:

  • xढेर को धक्का
  • कॉल foo
  • xस्टैक से रिटर्न-वैल्यू रजिस्टर में पॉप

क्या मेरी विधानसभा गलत है? क्या यह किसी अतिरिक्त रजिस्टर के साथ खिलवाड़ करने से कम कुशल है? यदि उन दोनों का उत्तर "नहीं" है, तो जीसीसी या क्लैंग इस तरह से क्यों नहीं करते हैं?

गॉडबोल्ट लिंक


संपादित करें: यहां एक कम तुच्छ उदाहरण है, यह दिखाने के लिए कि चर का सार्थक उपयोग होने पर भी यह होता है:

long foo(long);

long bar(long x) {
    return foo(x * x) - x;
}

मैंने इसे प्राप्त किया:

bar:
        push    rbx
        mov     rbx, rdi
        imul    rdi, rdi
        call    foo
        sub     rax, rbx
        pop     rbx
        ret

मेरे पास यह है:

bar:
        push    rdi
        imul    rdi, rdi
        call    foo
        pop     rdi
        sub     rax, rdi
        ret

इस बार, यह केवल एक निर्देश बनाम दो है, लेकिन मूल अवधारणा एक ही है।

गॉडबोल्ट लिंक


4
दिलचस्प चूक से अनुकूलन हुआ।
फ़ूज

1
सबसे अधिक संभावना यह है कि पारित पैरामीटर का उपयोग किया जाएगा ताकि आप एक वाष्पशील रजिस्टर को सहेजना चाहते हैं और पास किए गए पैरामीटर को एक रजिस्टर में नहीं रख सकते हैं, स्टैक पर नहीं है क्योंकि बाद में उस पैरामीटर तक पहुंच रजिस्टर से तेज है। x को foo पास करें और आप इसे देखेंगे। इसलिए यह उनके स्टैक फ्रेम सेटअप का सिर्फ एक सामान्य हिस्सा है।
old_timer

मुझे यह देखने को मिला कि फू के बिना यह स्टैक का उपयोग नहीं करता है, इसलिए हाँ यह एक चूक ऑप्टिमाइज़ेशन है लेकिन किसी को कुछ जोड़ने की आवश्यकता होगी, फ़ंक्शन का विश्लेषण करें और यदि मान का उपयोग नहीं किया गया है और उस रजिस्टर के साथ कोई संघर्ष नहीं है (आम तौर पर वहां है)।
old_timer

भुजा बैकेंड gcc पर भी ऐसा करती है। तो संभावना नहीं बैकेंड
old_timer

क्लैंग 10 एक ही कहानी (आर्म बैकेंड)।
old_timer

जवाबों:


5

टी एल: डॉ:

  • कंपाइलर इंटर्नल शायद इस अनुकूलन को आसानी से देखने के लिए सेट नहीं किए गए हैं, और यह संभवतः केवल छोटे कार्यों के आसपास ही उपयोगी है, कॉल के बीच बड़े कार्यों के अंदर नहीं।
  • अधिकांश कार्यों को बनाने के लिए इनलाइनिंग ज्यादातर समय एक बेहतर समाधान है
  • fooRBX को बचाने / पुनर्स्थापित करने के लिए नहीं होने पर एक विलंबता बनाम थ्रूपुट ट्रेडऑफ़ हो सकता है।

कंपाइलर मशीनरी के जटिल टुकड़े हैं। वे मानव की तरह "स्मार्ट" नहीं हैं, और हर संभव अनुकूलन को खोजने के लिए महंगे एल्गोरिदम अक्सर अतिरिक्त संकलन समय में लागत के लायक नहीं होते हैं।

मैंने इसे GCC बग 69986 के रूप में सूचित किया - 2016 में वापस फैल / पुनः लोड करने के लिए पुश / पॉप का उपयोग करके -Obs के साथ छोटे कोड संभव ; GCC devs की कोई गतिविधि या उत्तर नहीं है। : /

थोड़ा संबंधित: जीसीसी बग 70408 - एक ही कॉल-संरक्षित रजिस्टर का पुन: उपयोग करने से कुछ मामलों में छोटे कोड मिलेंगे - संकलक देवों ने मुझे बताया कि जीसीसी के लिए यह अनुकूलन करने में सक्षम होने के लिए बड़ी मात्रा में काम लगेगा क्योंकि इसे मूल्यांकन के आदेश की आवश्यकता होती है दो foo(int)कॉल के आधार पर जो लक्ष्य को आसान बना देगा।


यदि foo अपने आप को सहेजता / पुनर्स्थापित नहीं करता rbxहै, तो थ्रूपुट (इंस्ट्रक्शन काउंट) बनाम अतिरिक्त स्टोर / रीलोड विलंबता के बीच x-> रिटेल डिपेंडेंसी चेन पर ट्रेडऑफ होता है।

आमतौर पर कंपाइलर्स थ्रूपुट के ऊपर विलंबता का पक्ष लेते हैं, उदाहरण के लिए imul reg, reg, 10(3-चक्र विलंबता, 1 / घड़ी थ्रूपुट) के बजाय 2x एलईए का उपयोग करते हुए , क्योंकि अधिकांश कोड का औसत स्काइलेक की तरह 4-चौड़ी पाइपलाइनों पर 4 यूओपी / घड़ी से काफी कम है। (अधिक निर्देश / यूओपी आरओबी में अधिक स्थान लेते हैं, यह कम करते हुए कि एक ही आउट-ऑफ-ऑर्डर विंडो कितनी दूर तक देख सकती है, हालांकि, और निष्पादन वास्तव में स्टालों के साथ फट गया है, जो शायद कम-से-4 यूओपी में से कुछ के लिए लेखांकन है / घड़ी की औसत।)

यदि fooआरबीएक्स को धक्का / पॉप करता है, तो विलंबता के लिए बहुत कुछ नहीं है। पुनर्स्थापना होने से पहले बस के retबजाय बस के बाद होता है शायद प्रासंगिक नहीं है, जब तक कि कोई retगलतफहमी या आई-कैश मिस नहीं करता है जो रिटर्न पते पर कोड लाने में देरी करता है।

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


तो हाँ push rdi/ इस मामले pop raxमें अधिक कुशल होगा , और यह संभवतः छोटे गैर-पत्ती कार्यों के लिए एक चूक अनुकूलन है, जो कॉल करने वाले को बचाने / पुनर्स्थापित करने के लिए अधिक निर्देशों के लिए अतिरिक्त स्टोर / पुनः लोड विलंबता के बीच क्या करता है और शेष के बीच संतुलन पर निर्भर करता है ।fooxrbx

स्टैक-लेड मेटाडेटा के लिए यह संभव है कि वह RSP में परिवर्तनों का प्रतिनिधित्व करे, जैसे कि यदि उसने स्टैक स्लॉट में sub rsp, 8फैल / पुनः लोड करने के लिए उपयोग किया था x। (लेकिन संकलक इस अनुकूलन को नहीं जानते हैं, या तो pushस्थान आरक्षित करने के लिए और एक चर को इनिशियलाइज़ करने के लिए। किसी सी / सी ++ कंपाइलर का उपयोग लोकल वैरिएबल बनाने के लिए पुश पॉप निर्देशों का उपयोग कर सकता है, बजाय केवल एक बार जासूसी करने के? और इससे भी अधिक के लिए। एक स्थानीय संस्करण बड़े .eh_frameस्टैक को मेटाडेटा के लिए ले जाएगा क्योंकि आप स्टैक पॉइंटर को प्रत्येक पुश के साथ अलग-अलग स्थानांतरित कर रहे हैं। यह कॉलर्स को संरक्षित-संरक्षित रेज को बचाने / बहाल करने के लिए पुश / पॉप का उपयोग करने से कंपाइलर्स को नहीं रोकता है, हालांकि।)


IDK यदि यह अनुकूलन देखने के लिए शिक्षण संकलक के लायक होगा

हो सकता है कि यह एक फंक्शन के आसपास हो, एक फंक्शन के अंदर एक कॉल के दौरान नहीं। और जैसा कि मैंने कहा, यह निराशावादी धारणा पर आधारित है जो fooवैसे भी RBX को बचाएगा / बहाल करेगा। (या थ्रूपुट के लिए अनुकूलन यदि आप जानते हैं कि x से रिटर्न मान के लिए विलंबता महत्वपूर्ण नहीं है। लेकिन संकलक यह नहीं जानते हैं और आमतौर पर विलंबता के लिए अनुकूलन करते हैं)।

यदि आप बहुत सारे कोड में निराशावादी धारणा बनाना शुरू कर देते हैं (जैसे कि फ़ंक्शन के अंदर एकल फ़ंक्शन कॉल के आसपास), तो आपको अधिक ऐसे मामले मिलने शुरू हो जाएंगे, जहां RBX को बचाया / पुनर्स्थापित नहीं किया गया है और आप लाभ उठा सकते हैं।

आप लूप में इस अतिरिक्त बचत / पुनर्स्थापना पुश / पॉप को भी नहीं चाहते हैं, बस लूप के बाहर RBX को सहेजें / पुनर्स्थापित करें और फ़ंक्शन कॉल करने वाले लूप में कॉल-संरक्षित रजिस्टरों का उपयोग करें। यहां तक ​​कि छोरों के बिना, सामान्य मामले में अधिकांश फ़ंक्शन कई फ़ंक्शन कॉल करते हैं। यह अनुकूलन विचार लागू हो सकता है यदि आप वास्तव में xकिसी भी कॉल के बीच प्रयोग नहीं करते हैं, सिर्फ पहले और आखिरी के बाद, अन्यथा आपको प्रत्येक के लिए 16-बाइट स्टैक संरेखण बनाए रखने की समस्या है callयदि आप एक के बाद एक पॉप कर रहे हैं दूसरे कॉल से पहले कॉल करें।

संकलक सामान्य रूप से छोटे कार्यों में महान नहीं हैं। लेकिन यह सीपीयू के लिए बहुत अच्छा नहीं है। गैर-इनलाइन फ़ंक्शन कॉल का अनुकूलन पर सबसे अच्छा समय पर प्रभाव पड़ता है, जब तक कि कंपाइलर्स कैली के इंटर्नल को नहीं देख सकते हैं और सामान्य से अधिक अनुमान लगा सकते हैं। एक गैर-इनलाइन फ़ंक्शन कॉल एक अंतर्निहित स्मृति अवरोधक है: एक कॉलर को यह मान लेना होगा कि कोई फ़ंक्शन किसी भी वैश्विक-सुलभ डेटा को पढ़ या लिख ​​सकता है, इसलिए ऐसे सभी संस्करणों को सी सार मशीन के साथ सिंक करना होगा। (एस्केप एनालिसिस कॉल के दौरान रजिस्टरों में स्थानीय लोगों को रखने की अनुमति देता है, यदि उनका पता फ़ंक्शन से बच नहीं गया है।) इसके अलावा, कंपाइलर को यह मान लेना होगा कि कॉल-क्लोब रजिस्टर्ड सभी क्लोएबर्ड हैं। यह x86-64 सिस्टम V में फ्लोटिंग पॉइंट के लिए बेकार है, जिसमें कोई कॉल-संरक्षित एक्सएमएम रजिस्टर नहीं है।

जैसे छोटे कार्य bar()उनके कॉलर में इनलाइनिंग से बेहतर हैं। साथ संकलित करें -fltoतो यह ज्यादातर मामलों में फ़ाइल सीमाओं के पार भी हो सकता है। (फ़ंक्शन पॉइंटर्स और साझा-लाइब्रेरी सीमाएँ इसे हरा सकती हैं।)


मुझे लगता है कि इन ऑप्टिमाइज़ेशन को करने के लिए एक कारण संकलक ने परेशान नहीं किया है, यह है कि कंपाइलर इंटर्नल में अलग-अलग कोड की एक पूरी गुच्छा की आवश्यकता होगी , सामान्य स्टैक बनाम रजिस्टर-आवंटन कोड से अलग है जो कॉल-संरक्षित को बचाने का तरीका जानता है। रजिस्टर और उनका उपयोग करें।

यानी इसे लागू करने के लिए बहुत काम करना होगा, और इसे बनाए रखने के लिए बहुत सारे कोड, और अगर यह ऐसा करने के बारे में अति-उत्साही हो जाता है तो यह बदतर कोड बना सकता है ।

और यह भी कि यह (उम्मीद है) महत्वपूर्ण नहीं है; यदि यह मायने रखता है, तो आपको barइसके कॉलर में inlining होना चाहिए , या इनलाइन fooकरना चाहिए bar। यह ठीक है जब तक कि विभिन्न प्रकार के बहुत सारे barकार्य नहीं होते हैं और fooबड़े होते हैं, और किसी कारण से वे अपने कॉलर्स में इनलाइन नहीं कर सकते हैं।


यकीन नहीं कर रहे हैं समझ में आ रहा है कि क्यों कुछ संकलक कोड का अनुवाद इस तरह से करते हैं, जब बेहतर उपयोग हो सकता है .., अगर अनुवाद में त्रुटि नहीं है। उदाहरण के लिए संभव है कि पूछें कि अजीब क्यों है (अनुकूलित नहीं) इस लूप को थ्रॉस्लेटेड किया गया , gcc, icc और यहां तक ​​कि msvc की तुलना करें
RbMm

1
@ आरबीएम: मैं आपकी बात नहीं समझता। यह क्लैग के लिए एक पूरी तरह से अलग चूक अनुकूलन की तरह दिखता है, इस प्रश्न के बारे में असंबंधित है। छूट गए अनुकूलन बग मौजूद हैं, और ज्यादातर मामलों में तय होना चाहिए। आगे बढ़ो और इसे bugs.llvm.org
पीटर

हां, मेरा कोड उदाहरण मूल प्रश्न से संबंधित नहीं है। बस अजीब का एक और उदाहरण (मेरे रूप के लिए) अनुवाद (और केवल एकल क्लैंग संकलक के लिए)। लेकिन परिणाम asm कोड वैसे भी सही है। न केवल सबसे अच्छा और eveen देशी नहीं gcc / icc / msvc
RbMm
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.