संकलक (या नहीं) संकलक एक गुणा में लूप के अतिरिक्त लूप का अनुकूलन क्यों नहीं कर सकता है?


133

यह एक ऐसा सवाल है जो मिस्टिकियल के शानदार जवाब को पढ़ते हुए दिमाग में आया : एक अनारक्षित सरणी की तुलना में सॉर्ट किए गए सरणी को संसाधित करना अधिक तेज़ क्यों है ?

शामिल प्रकारों के लिए संदर्भ:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

अपने जवाब में वे बताते हैं कि इंटेल कंपाइलर (ICC) इस का अनुकूलन करता है:

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

... इसके बराबर में:

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

ऑप्टिमाइज़र पहचान रहा है कि ये बराबर हैं और इसलिए छोरों का आदान-प्रदान कर रहे हैं, शाखा को आंतरिक लूप के बाहर स्थानांतरित कर रहे हैं। बहुत चालाक!

लेकिन यह ऐसा क्यों नहीं करता है?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

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


14
यह कुछ ऐसा है जो शायद केवल इंटेल जानता है। मुझे नहीं पता कि यह अपना अनुकूलन पास किस क्रम से चलाता है। और जाहिर है, यह लूप-इंटरचेंज के बाद लूप-कोलैप्सिंग पास नहीं चलाता है।
मिस्ट्रीज

7
यह अनुकूलन केवल तभी मान्य है जब डेटा सरणी में निहित मान अपरिवर्तनीय हैं। उदाहरण के लिए, यदि मेमोरी में किसी इनपुट / आउटपुट डिवाइस पर हर बार डेटा पढ़ने के दौरान [0] एक अलग मान उत्पन्न होता है ...
थॉमस सीजी डी विलेना

2
यह किस प्रकार का डेटा है, पूर्णांक या फ्लोटिंग-पॉइंट? फ़्लोटिंग-पॉइंट में बार-बार जोड़ा जाना गुणन से बहुत अलग परिणाम देता है।
बेन वोइगट

6
@ थोमस: यदि डेटा थे volatile, तो लूप इंटरचेंज एक अमान्य ऑप्टिमाइज़ेशन होगा।
बेन वोइगट

3
GNAT (GCC 4.6 के साथ Ada संकलक) O3 पर लूप को स्विच नहीं करेगा, लेकिन यदि लूप स्विच किए जाते हैं, तो यह इसे गुणा में बदल देगा।
अभियोजन पक्ष

जवाबों:


105

संकलक आम तौर पर रूपांतरित नहीं कर सकता है

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

में

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

क्योंकि उत्तरार्द्ध हस्ताक्षरित पूर्णांक के अतिप्रवाह का कारण बन सकता है जहां पूर्व नहीं है। यहां तक ​​कि हस्ताक्षरित दो के पूरक पूर्णांक के अतिप्रवाह के लिए गारंटीकृत रैप-अराउंड व्यवहार के साथ, यह परिणाम को बदल देगा (यदि data[c]30000 है, तो उत्पाद लपेटो के साथ -1294967296ठेठ 32-बिट intएस के लिए बन जाएगा , जबकि 100000 बार 30000 को जोड़ने के लिए sum, यदि हो सकता है) ओवरफ्लो नहीं होता है, sum3000000000 की वृद्धि )। ध्यान दें कि एक ही अहस्ताक्षरित मात्राओं के लिए, अलग-अलग संख्याओं के साथ, अतिप्रवाह 100000 * data[c]आमतौर पर एक कमी modulo पेश करेगा 2^32जो अंतिम परिणाम में प्रकट नहीं होना चाहिए।

यह इसे में बदल सकता है

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

हालांकि, अगर, हमेशा की तरह, long longपर्याप्त रूप से बड़ा है int

यह ऐसा क्यों नहीं करता है, मैं नहीं बता सकता, मुझे लगता है कि यह वही है जो मिस्टिकियल ने कहा , "जाहिर है, यह लूप-इंटरचेंज के बाद लूप- कोलैप्सिंग पास नहीं चलाता है"।

ध्यान दें कि लूप-इंटरचेंज स्वयं आमतौर पर मान्य नहीं है (हस्ताक्षरित पूर्णांक के लिए), चूंकि

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

जहां अतिप्रवाह हो सकता है

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

नहीं होगा। यह यहाँ कोषेर है, क्योंकि हालत यह सुनिश्चित करती है data[c]कि सभी जोड़े गए समान चिन्ह हों, इसलिए यदि कोई ओवरफ्लो करता है, तो दोनों करते हैं।

मुझे यह भी सुनिश्चित नहीं होगा कि संकलक ने इसे ध्यान में रखा, हालांकि (@Mysticial, क्या आप ऐसी स्थिति की कोशिश कर सकते हैं जैसे data[c] & 0x80कि सकारात्मक और नकारात्मक मूल्यों के लिए सही हो सकता है?)। मेरे पास कंपाइलर अमान्य ऑप्टिमाइज़ेशन थे (उदाहरण के लिए, कुछ साल पहले, मेरे पास ICC (11.0, iirc) था , 1.0/nजहां साइन-इन-बिट-डबल-टू-डबल रूपांतरण का उपयोग किया nगया था , जहां एक था unsigned int। वह लगभग दोगुना तेज़ था । आउटपुट। लेकिन गलत है, बहुत सारे मूल्य 2^31ओह से अधिक बड़े थे ।)।


4
मुझे MPW कंपाइलर का एक संस्करण याद है जिसमें 32K से बड़े स्टैक फ्रेम की अनुमति देने का विकल्प जोड़ा गया था [पहले के संस्करण स्थानीय वेरिएबल्स के लिए @ A7 + int16 का उपयोग करके सीमित थे]। यह 32K से नीचे या 64K से अधिक के स्टैक फ्रेम के लिए सब कुछ सही है, लेकिन 40K स्टैक फ्रेम के लिए यह उपयोग करेगा ADD.W A6,$A000, यह भूलकर कि पता रजिस्टरों के साथ शब्द संचालन ऐड को बढ़ाने से पहले शब्द को 32 बिट्स पर हस्ताक्षर करते हैं। समस्या निवारण के लिए थोड़ी देर लग गई, क्योंकि केवल एक ही चीज़ के बीच कोड था ADDऔर अगली बार जब यह पॉप से ​​A6 पॉप अप हो गया, तो कॉल करने वाले के रजिस्टरों को पुनर्स्थापित करना यह उस फ्रेम में सहेज दिया गया है ...
सुपरकैट

3
... और केवल रजिस्टर करने वाले के बारे में परवाह करने के लिए हुआ एक स्थिर सरणी का [लोड-टाइम स्थिर] पता था। संकलक को पता था कि सरणी का पता एक रजिस्टर में सहेजा गया था, इसलिए यह उस पर आधारित अनुकूलन कर सकता है, लेकिन डिबगर बस एक स्थिरांक का पता जानता था। इस प्रकार, एक बयान से पहले MyArray[0] = 4;मैं इस विवरण को जोड़ सकता हूं MyArray, और निष्पादित किए जाने से पहले और बाद में उस स्थान को देख सकता हूं ; यह नहीं बदलेगा। कोड कुछ ऐसा था move.B @A3,#4और A3 हमेशा MyArrayकिसी भी समय इंगित करने वाला था जिसे निर्देश निष्पादित किया गया था, लेकिन यह नहीं हुआ। आनंद।
सुपरकाट

फिर क्लैंग इस तरह का अनुकूलन क्यों करता है?
जेसन एस

संकलक अपने आंतरिक मध्यवर्ती अभ्यावेदन में फिर से लिख सकता है, क्योंकि इसकी आंतरिक मध्यवर्ती अभ्यावेदन में कम अपरिभाषित व्यवहार की अनुमति है।
user253751

48

यह उत्तर विशिष्ट मामले से जुड़ा हुआ नहीं है, लेकिन यह प्रश्न शीर्षक पर लागू होता है और भविष्य के पाठकों के लिए दिलचस्प हो सकता है:

परिमित परिशुद्धता के कारण, दोहराया फ़्लोटिंग-पॉइंट जोड़ गुणा के बराबर नहीं है । विचार करें:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

डेमो


10
यह पूछे गए सवाल का कोई जवाब नहीं है। दिलचस्प जानकारी के बावजूद (और किसी भी C / C ++ प्रोग्रामर के लिए पता होना चाहिए), यह कोई मंच नहीं है, और यहाँ से संबंधित नहीं है।
orlp

30
@ नाइटक्रैकर: स्टैकऑवरफ्लो का घोषित लक्ष्य भविष्य के उपयोगकर्ताओं के लिए उपयोगी उत्तरों की खोज योग्य लाइब्रेरी का निर्माण करना है। और यह पूछे गए प्रश्न का उत्तर है ... यह सिर्फ इतना होता है कि कुछ अस्थिर जानकारी है जो मूल पोस्टर के लिए इस उत्तर को लागू नहीं करता है। यह अभी भी एक ही प्रश्न के साथ दूसरों के लिए लागू हो सकता है।
Ben Voigt

12
यह प्रश्न शीर्षक का उत्तर हो सकता है , लेकिन प्रश्न नहीं, नहीं।
orlp

7
जैसा कि मैंने कहा, यह रोचक जानकारी है। फिर भी यह मुझे गलत लगता है कि नोटा सवाल के शीर्ष उत्तर को इस सवाल का जवाब नहीं देता है क्योंकि यह अब खड़ा है । इसका कारण यह नहीं है कि इंटेल कंपाइलर ने ऑप्टिमाइज़ नहीं करने का फैसला किया है, बस्ता।
orlp

4
@ नाइटक्रैकर: यह मेरे लिए भी गलत लगता है कि यह शीर्ष उत्तर है। मैं उम्मीद कर रहा हूं कि कोई पूर्णांक मामले के लिए एक बहुत अच्छा जवाब पोस्ट करता है जो स्कोर में इस एक को पार करता है। दुर्भाग्य से, मुझे नहीं लगता कि पूर्णांक मामले के लिए "नहीं" के लिए एक जवाब है, क्योंकि परिवर्तन कानूनी होगा, इसलिए हम "ऐसा क्यों नहीं करते" के साथ छोड़ दिया जाता है, जो वास्तव में "गलत" है बहुत स्थानीयकृत "करीबी कारण, क्योंकि यह एक विशेष संकलक संस्करण के लिए अजीब है। मैंने जो प्रश्न दिया वह अधिक महत्वपूर्ण है, IMO।
बेन वोइग्ट

6

कंपाइलर में विभिन्न पास होते हैं जो अनुकूलन करता है। आमतौर पर प्रत्येक पास में या तो बयानों पर एक अनुकूलन या लूप ऑप्टिमाइज़ेशन किया जाता है। वर्तमान में ऐसा कोई मॉडल नहीं है जो लूप हेडर के आधार पर लूप बॉडी का अनुकूलन करता है। यह पता लगाना कठिन है और कम आम है।

जो अनुकूलन किया गया था वह लूप इनवेरिएंट कोड मोशन था। यह तकनीक के एक सेट का उपयोग करके किया जा सकता है।


4

ठीक है, मुझे लगता है कि कुछ कंपाइलर इस प्रकार के अनुकूलन कर सकते हैं, यह मानते हुए कि हम इंटेगर एरिथमेटिक्स के बारे में बात कर रहे हैं।

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

ऐतिहासिक रूप से, जीसीसी ने खुद को एक संकलक के रूप में दिखाया है जिसके पास इस तरह के कठोर कदम उठाने के लिए क्या है, लेकिन अन्य संकलक कथित "उपयोगकर्ता-इच्छित" व्यवहार के साथ रहना पसंद कर सकते हैं भले ही वह भाषा द्वारा अपरिभाषित हो।


मैं जानना चाहता हूं कि क्या मैं गलती से अपरिभाषित व्यवहार पर निर्भर हूं, लेकिन मुझे लगता है कि कंपाइलर को यह जानने का कोई तरीका नहीं है कि अतिप्रवाह एक रन-टाइम मुद्दा होगा: /
jhabbott

2
@ झब्बोट: यदि ओवरफ्लो होता है, तो अपरिभाषित व्यवहार होता है। व्यवहार को परिभाषित किया गया है या नहीं, जब तक कि रनटाइम तक अज्ञात है (संख्या क्रम में इनपुट हैं): पी।
orlp

3

अब यह करता है - कम से कम, क्लैंग करता है :

long long add_100k_signed(int *data, int arraySize)
{
    long long sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

-O1 से संकलन करता है

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        movsxd  rdx, dword ptr [rdi + 4*rsi]
        imul    rcx, rdx, 100000
        cmp     rdx, 127
        cmovle  rcx, r8
        add     rax, rcx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

पूर्णांक अतिप्रवाह का इससे कोई लेना-देना नहीं है; यदि पूर्णांक अतिप्रवाह है जो अपरिभाषित व्यवहार का कारण बनता है, तो यह किसी भी स्थिति में हो सकता है। यहाँ एक ही तरह का फंक्शन intदिया गया हैlong :

int add_100k_signed(int *data, int arraySize)
{
    int sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

-O1 से संकलन करता है

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        mov     edx, dword ptr [rdi + 4*rsi]
        imul    ecx, edx, 100000
        cmp     edx, 127
        cmovle  ecx, r8d
        add     eax, ecx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

2

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


3
बंद-फॉर्म गणना के साथ लूप को बदलना भी ताकत में कमी है, है ना?
Ben Voigt

औपचारिक रूप से, हाँ, मुझे लगता है, लेकिन मैंने कभी किसी को इस बारे में बात करते नहीं सुना। (हालांकि, साहित्य पर मैं थोड़ा पुराना हूँ।)
zwol

1

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

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.