gcc - जीसीसी एक*ए*ए*ए*ए*ए(ए*ए*ए)*(ए*ए*ए) को अनुकूलित क्यों नहीं करता है?




assembly floating-point (8)

इस प्रश्न के पहले से ही कुछ अच्छे जवाब हैं, लेकिन पूर्णता के लिए मैं यह इंगित करना चाहता था कि सी मानक का लागू खंड 5.1.2.2.3 / 15 है (जो धारा 1.9 / 9 के समान है) सी ++ 11 मानक)। इस खंड में कहा गया है कि ऑपरेटरों को केवल पुन: समूहित किया जा सकता है यदि वे वास्तव में सहयोगी या कम्यूटिव हैं।

मैं एक वैज्ञानिक अनुप्रयोग पर कुछ संख्यात्मक अनुकूलन कर रहा हूं। एक बात मैंने देखी है कि जीसीसी कॉल pow(a,2) को a*a में संकलित करके अनुकूलित करेगा, लेकिन कॉल pow(a,6) अनुकूलित नहीं है और वास्तव में लाइब्रेरी फ़ंक्शन pow कॉल करेगा, जो बहुत धीमा हो जाता है प्रदर्शन। (इसके विपरीत, इंटेल सी ++ कंपाइलर , निष्पादन योग्य icc , pow(a,6) लिए लाइब्रेरी कॉल को खत्म कर देगा।)

मैं इस बारे में उत्सुक हूं कि जब मैंने pow(a,6) को a*a*a*a*a*a जीसीसी 4.5.1 और विकल्पों " -O3 -lm -funroll-loops -msse4 " का उपयोग -O3 -lm -funroll-loops -msse4 , तो यह उपयोग करता है 5 mulsd निर्देश:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

जबकि अगर मैं लिखता हूं (a*a*a)*(a*a*a) , यह उत्पादन करेगा

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

जो गुणा निर्देशों की संख्या को कम करता है 3. icc के समान व्यवहार है।

संकलक इस अनुकूलन चाल को क्यों पहचानते हैं?


एक और समान मामला: अधिकांश कंपाइलर a + b + c + d (a + b) + (c + d) को अनुकूलित नहीं करेंगे (यह एक अनुकूलन है क्योंकि दूसरी अभिव्यक्ति को बेहतर ढंग से पाइपलाइन किया जा सकता है) और इसे मूल्यांकन के रूप में मूल्यांकन करें (यानी के रूप में (((a + b) + c) + d) )। यह भी कोने के मामलों के कारण है:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

यह 1.000000e-05 0.000000e+00 आउटपुट करता है


क्योंकि 32-बिट फ़्लोटिंग-पॉइंट नंबर - जैसे कि 1.024 - 1.024 नहीं है। कंप्यूटर में, 1.024 एक अंतराल है: (1.024-ई) से (1.024 + ई), जहां "ई" एक त्रुटि का प्रतिनिधित्व करता है। कुछ लोग इसका एहसास करने में असफल रहते हैं और यह भी मानते हैं कि * एक * में उन संख्याओं से जुड़ी त्रुटियों के बिना मनमानी-परिशुद्धता संख्याओं के गुणा के लिए खड़ा है। कुछ लोगों को इसका एहसास करने में असफल होने का कारण शायद गणित की गणना प्राथमिक विद्यालयों में होती है: बिना किसी त्रुटि के आदर्श संख्याओं के साथ काम करना, और यह मानना ​​कि गुणा करने के दौरान "ई" को अनदेखा करना ठीक है। उन्हें "फ्लोट ए = 1.2", "ए * ए * ए" और इसी तरह के सी कोड में "ई" निहित दिखाई नहीं देता है।

क्या अधिकांश प्रोग्रामर इस विचार को पहचान सकते हैं (और निष्पादित करने में सक्षम हैं) कि सी अभिव्यक्ति ए * ए * ए * ए * ए * वास्तव में आदर्श संख्याओं के साथ काम नहीं कर रही है, तो जीसीसी कंपाइलर "ए * ए को अनुकूलित करने के लिए स्वतंत्र होगा * ए * ए * ए * ए "कहने में" टी = (ए * ए); टी * टी * टी "जिसके लिए गुणा की एक छोटी संख्या की आवश्यकता होती है। लेकिन दुर्भाग्यवश, जीसीसी कंपाइलर को यह नहीं पता कि कोडर लिखने वाला प्रोग्रामर सोचता है कि "ए" एक त्रुटि है या बिना किसी त्रुटि के। और इसलिए जीसीसी केवल वही करेगा जो स्रोत कोड जैसा दिखता है - क्योंकि जीसीसी अपनी "नग्न आंख" के साथ देखता है।

... एक बार जब आप जानते हैं कि आप किस प्रकार के प्रोग्रामर हैं, तो आप जीसीसी को यह बताने के लिए "-फैस्ट-गणित" स्विच का उपयोग कर सकते हैं कि "अरे, जीसीसी, मुझे पता है कि मैं क्या कर रहा हूं!"। यह जीसीसी को एक * ए * ए * ए * ए * ए को टेक्स्ट के एक अलग टुकड़े में परिवर्तित करने की अनुमति देगा - यह * ए * ए * ए * ए * ए से अलग दिखता है - लेकिन अभी भी त्रुटि अंतराल के भीतर एक संख्या की गणना करता है एक * एक * एक * एक * एक * एक। यह ठीक है, क्योंकि आप पहले ही जानते हैं कि आप अंतराल के साथ काम कर रहे हैं, आदर्श संख्या नहीं।


क्योंकि फ़्लोटिंग प्वाइंट मैथ एसोसिएटिव नहीं है । फ्लोटिंग पॉइंट गुणा में ऑपरेंड को समूहबद्ध करने का तरीका उत्तर की संख्यात्मक सटीकता पर प्रभाव डालता है।

नतीजतन, अधिकांश कंपाइलर फ़्लोटिंग पॉइंट गणनाओं को पुन: व्यवस्थित करने के बारे में बहुत रूढ़िवादी हैं, जब तक कि वे सुनिश्चित न हों कि उत्तर वही रहेगा, या जब तक कि आप उन्हें न बताएं कि आपको संख्यात्मक सटीकता की परवाह नहीं है। उदाहरण के लिए: जीसीसी का -fassociative-math विकल्प जो जीसीसी को फ्लोटिंग पॉइंट ऑपरेशंस को फिर से -ffast-math करने की अनुमति देता है, या यहां तक ​​कि -ffast-math विकल्प जो गति के खिलाफ सटीकता के और भी आक्रामक -ffast-math अनुमति देता है।


जीसीसी वास्तव में एक * ए * ए * ए * ए * ए (ए * ए * ए) * (ए * ए * ए) अनुकूलित करता है जब एक पूर्णांक होता है। मैंने इस आदेश के साथ प्रयास किया:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

बहुत सारे जीसीसी झंडे हैं लेकिन कुछ भी कल्पना नहीं है। उनका मतलब है: stdin से पढ़ें; ओ 2 अनुकूलन स्तर का उपयोग करें; बाइनरी के बजाय आउटपुट असेंबली भाषा लिस्टिंग; लिस्टिंग इंटेल असेंबली भाषा वाक्यविन्यास का उपयोग करना चाहिए; इनपुट सी भाषा में है (आमतौर पर भाषा इनपुट फ़ाइल एक्सटेंशन से अनुमानित है, लेकिन stdin से पढ़ने पर कोई फ़ाइल एक्सटेंशन नहीं है); और stdout लिखो।

आउटपुट का महत्वपूर्ण हिस्सा यहां दिया गया है। मैंने कुछ टिप्पणियों के साथ टिप्पणी की है जो बताती है कि असेंबली भाषा में क्या हो रहा है:

    ; x is in edi to begin with.  eax will be used as a temporary register.
    mov    eax, edi     ; temp1 = x
    imul    eax, edi    ; temp2 = x * temp1
    imul    eax, edi    ; temp3 = x * temp2
    imul    eax, eax    ; temp4 = temp3 * temp3

मैं लिनक्स मिंट 16 पेट्रा, एक उबंटू व्युत्पन्न पर सिस्टम जीसीसी का उपयोग कर रहा हूं। यहां जीसीसी संस्करण है:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

जैसा कि अन्य पोस्टर्स ने नोट किया है, फ्लोटिंग पॉइंट में यह विकल्प संभव नहीं है, क्योंकि फ्लोटिंग पॉइंट अंकगणित वास्तव में सहयोगी नहीं है।


जीसीसी वास्तव में फ्लोटिंग-पॉइंट नंबरों के लिए भी इस अनुकूलन को कर सकता है। उदाहरण के लिए,

double foo(double a) {
  return a*a*a*a*a*a;
}

हो जाता है

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

-O -funsafe-math-optimizations । यह पुनरावृत्ति आईईईई -754 का उल्लंघन करती है, हालांकि, इसे ध्वज की आवश्यकता है।

हस्ताक्षर किए गए पूर्णांक, जैसा कि पीटर कॉर्डिस ने एक टिप्पणी में बताया है, बिना -funsafe-math-optimizations इस अनुकूलन को कर सकते हैं क्योंकि यह तब होता है जब कोई ओवरफ़्लो नहीं होता है और यदि ओवरफ़्लो होता है तो आपको अपरिभाषित व्यवहार मिलता है। तो आप मिलते हैं

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

बस के साथ- -O । हस्ताक्षरित पूर्णांक के लिए, यह भी आसान है क्योंकि वे 2 की आधुनिक शक्तियों का काम करते हैं और इसलिए अतिप्रवाह के चेहरे पर भी स्वतंत्र रूप से पुन: व्यवस्थित किया जा सकता है।


मुझे उम्मीद नहीं थी कि इस मामले को अनुकूलित किया जा सके। यह अक्सर नहीं हो सकता है जहां एक अभिव्यक्ति में उप-अभिव्यक्तियां होती हैं जिन्हें पूरे परिचालन को हटाने के लिए पुन: समूहित किया जा सकता है। मैं संकलक लेखकों को उन क्षेत्रों में अपना समय निवेश करने की उम्मीद करूंगा जो शायद ही कभी सामना किए जाने वाले किनारे के मामले को कवर करने के बजाए ध्यान देने योग्य सुधारों के परिणामस्वरूप हों।

मैं अन्य उत्तरों से सीखने में हैरान था कि इस अभिव्यक्ति को वास्तव में उचित कंपाइलर स्विच के साथ अनुकूलित किया जा सकता है। या तो अनुकूलन छोटा है, या यह एक बहुत अधिक सामान्य अनुकूलन का एक बढ़त मामला है, या संकलक लेखकों को बहुत गहन थे।

जैसा कि आपने यहां किया है, संकलक को संकेत प्रदान करने में कुछ भी गलत नहीं है। बयान और अभिव्यक्तियों को पुनर्व्यवस्थित करने के लिए माइक्रो-ऑप्टिमाइज़ेशन प्रक्रिया का यह एक सामान्य और अपेक्षित हिस्सा है, यह देखने के लिए कि वे क्या अंतर लाएंगे।

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


लाइब्रेरी फ़ंक्शंस जैसे "पाउ" आमतौर पर न्यूनतम संभव त्रुटि (सामान्य मामले में) उत्पन्न करने के लिए सावधानीपूर्वक तैयार की जाती हैं। यह आम तौर पर स्प्लिंस के साथ अनुमानित कार्यों को प्राप्त किया जाता है (पास्कल की टिप्पणी के अनुसार सबसे आम कार्यान्वयन रेमेज़ एल्गोरिदम का उपयोग करना प्रतीत होता है)

मूल रूप से निम्नलिखित ऑपरेशन:

pow(x,y);

किसी भी एकल गुणा या विभाजन में त्रुटि के रूप में लगभग समान परिमाण की अंतर्निहित त्रुटि है।

जबकि निम्नलिखित ऑपरेशन:

float a=someValue;
float b=a*a*a*a*a*a;

एक अंतर्निहित त्रुटि है जो एकल गुणा या विभाजन की त्रुटि से 5 गुना अधिक है (क्योंकि आप 5 गुणाओं को जोड़ रहे हैं)।

संकलक वास्तव में इस तरह के अनुकूलन के लिए सावधान रहना चाहिए:

  1. यदि a*a*a*a*a*a pow(a,6) को अनुकूलित करना a*a*a*a*a*a प्रदर्शन में सुधार कर सकता है , लेकिन फ्लोटिंग पॉइंट नंबरों के लिए सटीकता को कम कर देता है।
  2. यदि a*a*a*a*a*a को a*a*a*a*a*a pow(a,6) अनुकूलित a*a*a*a*a*a वास्तव में सटीकता को कम कर सकता है क्योंकि "ए" कुछ विशेष मूल्य था जो त्रुटि के बिना गुणा की अनुमति देता है (2 या कुछ छोटे पूर्णांक संख्या की शक्ति)
  3. यदि pow(a,6) से (a*a*a)*(a*a*a) या (a*a)*(a*a)*(a*a) अनुकूलित करना अभी भी सटीकता का नुकसान हो सकता है pow समारोह की तुलना में।

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

एकमात्र चीज जो समझ में आती है (व्यक्तिगत राय, और स्पष्ट रूप से जीसीसी में किसी विशेष ऑप्टिमाइज़ेशन या कंपाइलर फ्लैग के विकल्प को अनुकूलित करने के लिए) "ए * ए" के साथ "पाउ (ए, 2)" को प्रतिस्थापित किया जाना चाहिए। यह एकमात्र सायन चीज होगी जो एक कंपाइलर विक्रेता को करना चाहिए।





fast-math