c - जीसीसी लगभग उसी सी कोड के लिए ऐसी मूल रूप से अलग असेंबली क्यों उत्पन्न करता है?




optimization gcc (2)

एक अनुकूलित ftol समारोह लिखते समय मुझे GCC 4.6.1 में कुछ अजीब व्यवहार मिला। मुझे आपको पहले कोड दिखाएं (स्पष्टता के लिए मैंने मतभेदों को चिह्नित किया):

fast_trunc_one, सी:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;                       /* diff */
    } else {
        r = mantissa >> exponent;                        /* diff */
    }

    return (r ^ -sign) + sign;                           /* diff */
}

fast_trunc_two, सी:

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent) ^ -sign;             /* diff */
    } else {
        r = (mantissa >> exponent) ^ -sign;              /* diff */
    }

    return r + sign;                                     /* diff */
}

वही सही लगता है? खैर जीसीसी असहमत है। gcc -O3 -S -Wall -o test.s test.c साथ संकलन के बाद यह असेंबली आउटपुट है:

fast_trunc_one, जेनरेट किया गया:

_fast_trunc_one:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %edx
    andl    $8388607, %edx
    sarl    $23, %eax
    orl $8388608, %edx
    andl    $255, %eax
    subl    %eax, %ecx
    movl    %edx, %eax
    sarl    %cl, %eax
    testl   %ecx, %ecx
    js  L5
    rep
    ret
    .p2align 4,,7
L5:
    negl    %ecx
    movl    %edx, %eax
    sall    %cl, %eax
    ret
    .cfi_endproc

fast_trunc_two, जेनरेट किया गया:

_fast_trunc_two:
LFB1:
    .cfi_startproc
    pushl   %ebx
    .cfi_def_cfa_offset 8
    .cfi_offset 3, -8
    movl    8(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %ebx
    movl    %eax, %edx
    sarl    $23, %ebx
    andl    $8388607, %edx
    andl    $255, %ebx
    orl $8388608, %edx
    andl    $-2147483648, %eax
    subl    %ebx, %ecx
    js  L9
    sarl    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_remember_state
    .cfi_def_cfa_offset 4
    .cfi_restore 3
    ret
    .p2align 4,,7
L9:
    .cfi_restore_state
    negl    %ecx
    sall    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_restore 3
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc

यह एक बहुत अंतर है। यह वास्तव में प्रोफ़ाइल पर भी fast_trunc_one है, fast_trunc_one की fast_trunc_one में लगभग 30% तेज है। अब मेरा सवाल: इसका कारण क्या है?


यह कंपाइलर्स की प्रकृति है। मान लीजिए कि वे सबसे तेज़ या सबसे अच्छा रास्ता लेंगे, काफी झूठा है। कोई भी जो इसका तात्पर्य है कि आपको अनुकूलित करने के लिए अपने कोड में कुछ भी करने की ज़रूरत नहीं है क्योंकि "आधुनिक कंपाइलर्स" रिक्त स्थान भरते हैं, सबसे अच्छा काम करते हैं, सबसे तेज़ कोड बनाते हैं। असल में मैंने देखा कि जीसीसी 3.x से 4 तक खराब हो गया है। कम से कम हाथ पर एक्स। 4.x इस बिंदु से 3.x तक पकड़ा हो सकता है, लेकिन इसके शुरुआती दिनों में धीमे कोड का उत्पादन हुआ। अभ्यास के साथ आप सीख सकते हैं कि अपना कोड कैसे लिखना है ताकि संकलक को कड़ी मेहनत करनी पड़े और परिणामस्वरूप अधिक सुसंगत और अपेक्षित नतीजे सामने आए।

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

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

क्या आपने देखा कि जीसीसी उत्पादन के विभिन्न संस्करण क्या हैं? 3.x और 4.x विशेष रूप से 4.5 बनाम 4.6 बनाम 4.7, आदि? और विभिन्न लक्ष्य प्रोसेसर, x86, arm, mips, आदि या x86 के विभिन्न स्वादों के लिए यदि आप मूल कंपाइलर का उपयोग करते हैं, 32 बिट बनाम 64 बिट, आदि? और फिर विभिन्न लक्ष्यों के लिए llvm (clang)?

रहस्यवादी ने कोड का विश्लेषण / अनुकूलन करने की समस्या के माध्यम से काम करने के लिए आवश्यक विचार प्रक्रिया में उत्कृष्ट काम किया है, किसी भी "आधुनिक कंपाइलर" की अपेक्षा नहीं है, इसके साथ किसी भी संकलक के साथ आने की उम्मीद है।

गणित गुणों में प्रवेश किए बिना, इस फ़ॉर्म का कोड

if (exponent < 0) {
  r = mantissa << -exponent;                       /* diff */
} else {
  r = mantissa >> exponent;                        /* diff */
}
return (r ^ -sign) + sign;                           /* diff */

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

if (exponent < 0) {
  return((mantissa << -exponent)^-sign)+sign;
} else {
  return((mantissa << -exponent)^-sign)+sign;
}

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

यह जीसीसी स्रोत कोड में खोदने का एक सही अवसर है। ऐसा प्रतीत होता है कि आपको एक ऐसा मामला मिला है जहां अनुकूलक ने एक मामले में एक चीज देखी और फिर किसी अन्य मामले में एक और चीज देखी। फिर अगला कदम उठाएं और देखें कि क्या आप उस मामले को देखने के लिए जीसीसी प्राप्त नहीं कर सकते हैं। प्रत्येक अनुकूलन वहां होता है क्योंकि कुछ व्यक्ति या समूह ने अनुकूलन को पहचाना और जानबूझकर इसे वहां रखा। इस अनुकूलन के लिए वहां रहना और हर बार किसी को इसे वहां रखना है (और फिर इसका परीक्षण करें, और फिर इसे भविष्य में बनाए रखें)।

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

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

for(ra=0;ra<20;ra++) dummy(ra);

यह कहीं भी असेंबलर की 60-100 लाइनों के बीच उत्पादन किया। यह लूप अनलॉक किया। यदि आप इसके बारे में सोचते हैं, तो मैंने लाइनों की गणना नहीं की है, इसे जोड़ना है, परिणाम को फ़ंक्शन कॉल में इनपुट में कॉपी करना, फ़ंक्शन कॉल करना, तीन ऑपरेशन न्यूनतम करना है। तो उस लक्ष्य के आधार पर जो कम से कम 60 निर्देश हैं, 80 यदि प्रति लूप चार, 100 प्रति लूप, आदि।


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

उदाहरण के लिए, एलएलवीएम का clang कंपाइलर, दोनों कार्यों के लिए एक ही कोड देता है (फ़ंक्शन नाम को छोड़कर), देने:

_fast_trunc_two:                        ## @fast_trunc_one
        movl    %edi, %edx
        andl    $-2147483648, %edx      ## imm = 0xFFFFFFFF80000000
        movl    %edi, %esi
        andl    $8388607, %esi          ## imm = 0x7FFFFF
        orl     $8388608, %esi          ## imm = 0x800000
        shrl    $23, %edi
        movzbl  %dil, %eax
        movl    $150, %ecx
        subl    %eax, %ecx
        js      LBB0_1
        shrl    %cl, %esi
        jmp     LBB0_3
LBB0_1:                                 ## %if.then
        negl    %ecx
        shll    %cl, %esi
LBB0_3:                                 ## %if.end
        movl    %edx, %eax
        negl    %eax
        xorl    %esi, %eax
        addl    %edx, %eax
        ret

यह कोड ओपी से पहले जीसीसी संस्करण के रूप में छोटा नहीं है, लेकिन जब तक दूसरा नहीं है।

X86_64 के लिए संकलित करने वाले किसी अन्य कंपाइलर (जिसे मैं नाम नहीं दूंगा) से कोड, दोनों कार्यों के लिए इसका उत्पादन करता है:

fast_trunc_one:
        movl      %edi, %ecx        
        shrl      $23, %ecx         
        movl      %edi, %eax        
        movzbl    %cl, %edx         
        andl      $8388607, %eax    
        negl      %edx              
        orl       $8388608, %eax    
        addl      $150, %edx        
        movl      %eax, %esi        
        movl      %edx, %ecx        
        andl      $-2147483648, %edi
        negl      %ecx              
        movl      %edi, %r8d        
        shll      %cl, %esi         
        negl      %r8d              
        movl      %edx, %ecx        
        shrl      %cl, %eax         
        testl     %edx, %edx        
        cmovl     %esi, %eax        
        xorl      %r8d, %eax        
        addl      %edi, %eax        
        ret                         

जो आकर्षक है कि यह दोनों पक्षों की गणना करता है और फिर सही चुनने के अंत में एक सशर्त कदम का उपयोग करता है।

ओपन 64 कंपाइलर निम्न का उत्पादन करता है:

fast_trunc_one: 
    movl %edi,%r9d                  
    sarl $23,%r9d                   
    movzbl %r9b,%r9d                
    addl $-150,%r9d                 
    movl %edi,%eax                  
    movl %r9d,%r8d                  
    andl $8388607,%eax              
    negl %r8d                       
    orl $8388608,%eax               
    testl %r8d,%r8d                 
    jl .LBB2_fast_trunc_one         
    movl %r8d,%ecx                  
    movl %eax,%edx                  
    sarl %cl,%edx                   
.Lt_0_1538:
    andl $-2147483648,%edi          
    movl %edi,%eax                  
    negl %eax                       
    xorl %edx,%eax                  
    addl %edi,%eax                  
    ret                             
    .p2align 5,,31
.LBB2_fast_trunc_one:
    movl %r9d,%ecx                  
    movl %eax,%edx                  
    shll %cl,%edx                   
    jmp .Lt_0_1538                  

और समान, लेकिन समान नहीं, fast_trunc_two के लिए कोड।

वैसे भी, जब अनुकूलन की बात आती है, तो यह लॉटरी है - यह वही है ... यह जानना हमेशा आसान नहीं होता कि आप कोड को किसी विशेष तरीके से क्यों संकलित करते हैं।







compiler-optimization