c - क्‍लांग-O0(इस सरल फ्लोटिंग प्‍वाइंट राशि के लिए) के साथ अकुशल अस्‍पताल का निर्माण क्‍यों करता है?



assembly llvm (1)

मैं llvm clang Apple LLVM संस्करण 8.0.0 (clang-800.0.42.1.1) पर इस कोड को डिसाइड कर रहा हूं:

int main() {
    float a=0.151234;
    float b=0.2;
    float c=a+b;
    printf("%f", c);
}

मैंने बिना -O विशिष्टताओं के साथ संकलित किया, लेकिन मैंने -O0 के साथ भी कोशिश की (वही देता है) और -O2 (वास्तव में मूल्य की गणना करता है और इसे पूर्व-संग्रहित करता है)

परिणामी disassembly निम्नलिखित है (मैंने उन हिस्सों को हटा दिया जो प्रासंगिक नहीं हैं)

->  0x100000f30 <+0>:  pushq  %rbp
    0x100000f31 <+1>:  movq   %rsp, %rbp
    0x100000f34 <+4>:  subq   $0x10, %rsp
    0x100000f38 <+8>:  leaq   0x6d(%rip), %rdi       
    0x100000f3f <+15>: movss  0x5d(%rip), %xmm0           
    0x100000f47 <+23>: movss  0x59(%rip), %xmm1        
    0x100000f4f <+31>: movss  %xmm1, -0x4(%rbp)  
    0x100000f54 <+36>: movss  %xmm0, -0x8(%rbp)
    0x100000f59 <+41>: movss  -0x4(%rbp), %xmm0         
    0x100000f5e <+46>: addss  -0x8(%rbp), %xmm0
    0x100000f63 <+51>: movss  %xmm0, -0xc(%rbp)
    ...

जाहिरा तौर पर यह निम्नलिखित कर रहा है:

  1. xmm0 और xmm1 रजिस्टर पर दो फ्लोट लोड कर रहा है
  2. उन्हें स्टैक में डाल दिया
  3. स्टैक से xmm0 तक एक मान (एक xmm0 पहले नहीं था) लोड करें
  4. अतिरिक्त प्रदर्शन करें।
  5. परिणाम को वापस स्टैक पर संग्रहीत करें।

मुझे यह अयोग्य लगता है क्योंकि:

  1. सब कुछ रजिस्ट्री में किया जा सकता है। मैं बाद में ए और बी का उपयोग नहीं कर रहा हूं, इसलिए यह स्टैक को शामिल करने वाले किसी भी ऑपरेशन को छोड़ सकता है।
  2. यहां तक ​​कि अगर यह स्टैक का उपयोग करना चाहता है, तो यह स्टैक से xmm0 को फिर से लोड करने से बचा सकता है अगर यह एक अलग आदेश के साथ ऑपरेशन करता है।

यह देखते हुए कि संकलक हमेशा सही होता है, उसने इस रणनीति को क्यों चुना?


-O0 ( -O0 ) डिफॉल्ट है । यह संकलक को बताता है कि आप इसे तेजी से (कम संकलन समय) संकलित करना चाहते हैं, कुशल कोड बनाने के लिए अतिरिक्त समय संकलित करने के लिए नहीं।

( -O0 का शाब्दिक रूप से कोई अनुकूलन नहीं है; उदाहरण के लिए gcc अभी भी अंदर कोड को खत्म कर देगा if(1 == 2){ } ब्लॉक। विशेष रूप से अधिकांश अन्य संकलक से अधिक gcc अभी भी -O0 पर विभाजन के लिए गुणक व्युत्क्रमों का उपयोग करने जैसी चीजें करता है, क्योंकि यह अभी भी अपने सी स्रोत को तर्क के कई आंतरिक अभ्यावेदन के माध्यम से बदल देता है, आखिरकार asm छोड़ने से पहले।

साथ ही, "कंपाइलर हमेशा सही होता है" -O3 पर भी एक अतिशयोक्ति है। बड़े पैमाने पर कंपाइलर बहुत अच्छे हैं, लेकिन एकल छोरों के भीतर मामूली चूक-अनुकूलन अभी भी आम हैं। अक्सर बहुत कम प्रभाव के साथ, लेकिन एक लूप में व्यर्थ निर्देश (या उफ़) आउट-ऑफ-ऑर्डर निष्पादन रिडरिंग विंडो में जगह खा सकते हैं, और दूसरे धागे के साथ एक कोर साझा करते समय कम हाइपर-थ्रेडिंग अनुकूल हो सकते हैं। हाथ से लिखे विधानसभा की तुलना में Collatz अनुमान को तेजी से जांचने के लिए C ++ कोड देखें - क्यों? एक साधारण विशिष्ट मामले में संकलक की पिटाई के बारे में अधिक जानकारी के लिए।

इससे भी महत्वपूर्ण बात, -O0 का तात्पर्य सुसंगत डिबगिंग के लिए volatile समान सभी चर का इलाज करना भी है । तो इसलिए आप एक ब्रेकपॉइंट या सिंगल स्टेप सेट कर सकते हैं और C वैरिएबल के मान को संशोधित कर सकते हैं, और फिर निष्पादन जारी रख सकते हैं और प्रोग्राम को काम कर सकते हैं जिस तरह से आप अपने सी सोर्स से सी एब्सट्रैक्ट मशीन पर चलने की उम्मीद करेंगे। इसलिए कंपाइलर कोई निरंतर-प्रसार या मूल्य-श्रेणी सरलीकरण नहीं कर सकता है। (उदाहरण के लिए एक पूर्णांक जिसे गैर-नकारात्मक माना जाता है, इसका उपयोग करके चीजों को सरल बना सकते हैं, या कुछ बना सकते हैं यदि स्थितियां हमेशा सही या हमेशा गलत होती हैं।)

(यह volatile रूप में बहुत बुरा नहीं है : एक बयान के भीतर एक ही चर के कई संदर्भों का परिणाम हमेशा कई भारों में नहीं होता है; -O0 संकलक अभी भी एक अभिव्यक्ति के भीतर कुछ हद तक अनुकूलन करेंगे।)

कंपाइलर्स को विशेष रूप से स्टेटमेंट्स के बीच अपने मेमोरी एड्रेस पर सभी वेरिएबल्स को स्टोर / -O0 करके -O0 लिए विशेष रूप से एंटी-ऑप्टिमाइज़ करना -O0 है । (C और C ++ में, प्रत्येक चर का एक पता होता है जब तक कि इसे (अब अप्रचलित) register कीवर्ड के साथ घोषित नहीं किया गया था और इसका पता कभी नहीं लिया गया है। पते को अनुकूलित करना अन्य चर के लिए नियम के अनुसार संभव है, लेकिन isn 'टी-ओ 0 पर किया गया)

दुर्भाग्य से, डिबग-इंफो प्रारूप, रजिस्टरों के माध्यम से एक चर के स्थान को ट्रैक नहीं कर सकता है, इसलिए इस धीमे और बेवकूफ कोड-जीन के बिना पूरी तरह से सुसंगत डिबगिंग संभव नहीं है।

यदि आपको इसकी आवश्यकता नहीं है, तो आप लाइट ऑप्टिमाइज़ेशन के लिए -Og , और लगातार डीबगिंग के लिए आवश्यक एंटी-ऑप्टिमाइज़ेशन के बिना संकलन कर सकते हैं। जीसीसी मैनुअल इसे सामान्य रूप से संपादित / संकलन / चलाने के चक्र के लिए सुझाता है, लेकिन डिबगिंग करते समय आपको स्वचालित भंडारण के साथ कई स्थानीय चर के लिए "अनुकूलित आउट" मिलेगा। ग्लोबल्स और फ़ंक्शन आर्ग में अभी भी आमतौर पर अपने वास्तविक मूल्य होते हैं, कम से कम फ़ंक्शन सीमाओं पर।

इससे भी बदतर, -O0 कोड बनाता है जो अभी भी काम करता है भले ही आप एक अलग स्रोत लाइन पर निष्पादन जारी रखने के लिए GDB के jump कमांड का उपयोग करें । इसलिए प्रत्येक सी स्टेटमेंट को निर्देशों के एक पूरी तरह से स्वतंत्र ब्लॉक में संकलित किया जाना है। ( क्या GDB डिबगर में "कूदना" / "छोड़ना" संभव है? )

for() छोरों को मुहावरेदार (asm के लिए) do{}while() छोरों , और अन्य प्रतिबंधों में परिवर्तित नहीं किया जा सकता है।

उपरोक्त सभी कारणों से, (माइक्रो-) बेंचमार्किंग अन-ऑप्टिमाइज़्ड कोड समय की भारी बर्बादी है; परिणाम इस बात पर निर्भर करते हैं कि आपने उस स्रोत को कैसे लिखा है जो सामान्य अनुकूलन के साथ संकलित करने पर कोई फर्क नहीं पड़ता। -O0 बनाम -O0 प्रदर्शन रैखिक संबंधित नहीं है; कुछ कोड दूसरों की तुलना में बहुत अधिक गति करेंगे

-O0 कोड में -O0 अक्सर -O0 से अलग होंगी - अक्सर एक लूप काउंटर पर जो स्मृति में रखा जाता है, एक ~ 6-चक्र लूप-निर्भर निर्भरता श्रृंखला बनाता है। यह कंपाइलर-जनित एएसएम में दिलचस्प प्रभाव पैदा कर सकता है जैसे कि बिना अनुकूलन के संकलित किए जाने पर एक निरर्थक असाइनमेंट स्पीड को जोड़ना (जो एएसएम के दृष्टिकोण से दिलचस्प हैं, लेकिन सी के लिए नहीं )

"मेरे बेंचमार्क को अन्यथा अनुकूलित किया गया" -O0 कोड के प्रदर्शन को -O0 के लिए एक वैध औचित्य नहीं है। उदाहरण के लिए अंतिम असाइनमेंट के लिए C लूप ऑप्टिमाइज़ेशन सहायता देखें और -O0 लिए ट्यूनिंग खरगोश के छेद के बारे में अधिक विवरण।

दिलचस्प संकलक उत्पादन हो रही है

यदि आप यह देखना चाहते हैं कि कंपाइलर 2 वैरिएबल को कैसे जोड़ता है, तो एक फ़ंक्शन लिखें जो आर्ग्स लेता है और एक वैल्यू देता है । याद रखें कि आप केवल ऐश को देखना चाहते हैं, इसे चलाना नहीं चाहते हैं, इसलिए आपको किसी भी चीज के लिए एक main या किसी भी संख्यात्मक शाब्दिक मूल्यों की आवश्यकता नहीं है, जो एक रनटाइम चर होना चाहिए।

यह भी देखें कि जीसीसी / क्लैंग असेंबली आउटपुट से "शोर" कैसे निकालें? इस बारे में अधिक जानकारी के लिए।

float foo(float a, float b) {
    float c=a+b;
    return c;
}

clang -O3 ( Godbolt कंपाइलर एक्सप्लोरर पर ) के साथ उम्मीद की जाती है

    addss   xmm0, xmm1
    ret

लेकिन -O0 साथ यह मेमोरी को स्टैक करने के लिए -O0 फैलता है। (गॉडबोल्ट कंपाइलर द्वारा उत्सर्जित डिबग सूचना का उपयोग कलर-कोड asm निर्देशों के अनुसार करता है, जिसके अनुसार वे C से आए हैं। मैंने प्रत्येक स्टेटमेंट के लिए ब्लॉक दिखाने के लिए लाइन ब्रेक जोड़ा है, लेकिन आप इसे Godbolt लिंक पर रंग हाइलाइटिंग के साथ देख सकते हैं। अनुकूलित कंपाइलर आउटपुट में एक आंतरिक लूप के दिलचस्प भाग को खोजने के लिए बहुत आसान है।)

gcc -fverbose-asm हर लाइन पर टिप्पणियों को C नाम के रूप में ऑपरेंड नाम दिखाएगा। अनुकूलित कोड में जो अक्सर एक आंतरिक tmp नाम होता है, लेकिन संयुक्त राष्ट्र के अनुकूलित कोड में यह आमतौर पर C स्रोत से एक वास्तविक चर होता है। मैंने मैन्युअल रूप से क्लैंग आउटपुट पर टिप्पणी की है क्योंकि यह ऐसा नहीं करता है।

# clang7.0 -O0  also on Godbolt
foo:
    push    rbp
    mov     rbp, rsp                  # make a traditional stack frame
    movss   DWORD PTR [rbp-20], xmm0  # spill the register args
    movss   DWORD PTR [rbp-24], xmm1  # into the red zone (below RSP)

    movss   xmm0, DWORD PTR [rbp-20]  # a
    addss   xmm0, DWORD PTR [rbp-24]  # +b
    movss   DWORD PTR [rbp-4], xmm0   # store c

    movss   xmm0, DWORD PTR [rbp-4]   # return 0
    pop     rbp                       # epilogue
    ret

मजेदार तथ्य: register float c = a+b; वापसी मूल्य एक्सएमएम ० में बयानों के बीच में रह सकता है, इसके बजाय छिटपुट / पुनः लोड किया जा सकता है। चर का कोई पता नहीं है। (मैंने फ़ंक्शन के उस संस्करण को गॉडबोल्ट लिंक में शामिल किया है।)

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

सम्बंधित:





compiler-optimization