क्यों C कंपाइलर स्विच को ऑप्टिमाइज़ करते हैं और यदि अलग से


9

मैं हाल ही में एक व्यक्तिगत परियोजना पर काम कर रहा था जब मैं एक विषम मुद्दे पर ठोकर खाई।

बहुत तंग लूप में मेरे पास 0 और 15. के बीच के मान के साथ एक पूर्णांक है। मुझे मान 0, 1, 8, और 9 के लिए -1 और मान 4, 5, 12 और 13 के लिए 1 प्राप्त करने की आवश्यकता है।

मैं कुछ विकल्पों की जाँच करने के लिए गॉडबोल्ट की ओर मुड़ गया और आश्चर्यचकित था कि ऐसा लग रहा था कि कंपाइलर एक स्विच स्टेटमेंट को उसी तरह से ऑप्टिमाइज़ नहीं कर सकता, जैसे कि एक चेन।

लिंक यहाँ है: https://godbolt.org/z/WYVBFl

कोड है:

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}

int b(int num) {
    num &= 0xF;

    if (num == 0 || num == 1 || num == 8 || num == 9) 
        return -1;

    if (num == 4 || num == 5 || num == 12 || num == 13)
        return 1;

    return 0;
}

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
        default:
            return 0;
    }
}

मुझे लगता था कि b और c समान परिणाम देंगे, और मुझे उम्मीद थी कि मैं अपने समाधान के बाद से एक कुशल कार्यान्वयन के साथ आने के लिए बिट-हैक्स पढ़ सकता हूं (स्विच स्टेटमेंट - दूसरे रूप में) काफी धीमा था।

अजीब तरह से, bबिट-हैक्स के लिए संकलित किया cगया था, जबकि या तो बहुत अधिक अन-अनुकूलित किया गया था या के एक अलग मामले में कम किया गया थाa लक्ष्य हार्डवेयर आधार पर ।

क्या कोई समझा सकता है कि यह विसंगति क्यों है? इस क्वेरी को ऑप्टिमाइज़ करने का 'सही' तरीका क्या है?

संपादित करें:

स्पष्टीकरण

मैं चाहता हूं कि स्विच समाधान सबसे तेज़, या इसी तरह "स्वच्छ" समाधान हो। हालांकि जब मेरी मशीन पर अनुकूलन के साथ संकलित किया जाता है यदि समाधान काफी तेज होता है।

मैंने प्रदर्शित करने के लिए एक त्वरित कार्यक्रम लिखा और टीआईओ के पास वही परिणाम हैं जो मुझे स्थानीय रूप से मिलते हैं: इसे ऑनलाइन आज़माएं!

साथ static inlineलुकअप तालिका थोड़ा गति: यह ऑनलाइन कोशिश करो!


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

मेरा मुद्दा यह है कि मुझे इसे तेज करने की आवश्यकता है, लेकिन अगर समाधान अत्यधिक बनाए रखने योग्य नहीं है। क्या किसी क्लीनर के समाधान को पर्याप्त रूप से अनुकूलित करने के लिए कंपाइलर प्राप्त करने का कोई तरीका है? क्या कोई समझा सकता है कि वह इस मामले में ऐसा क्यों नहीं कर सकता?
लैंबडेबेटा

मैं कम से कम कार्यों को स्थैतिक, या बेहतर रूप में परिभाषित करके शुरू करूँगा- उन्हें इनलाइन करना।
वाइल्डप्लाशर

@wildplasser इसे गति प्रदान करता है, लेकिन फिर ifभी धड़कता है switch(अजीब तरह से देखने के बाद भी तेज हो जाता है) [TIO का अनुसरण करने के लिए]
LambdaBeta

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

जवाबों:


6

यदि आप सभी मामलों को स्पष्ट रूप से समझ लेते हैं, तो जीसीसी बहुत कुशल है:

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
            case 2: case 3: case 6: case 7: case 10: case 11: case 14: case 15: 
        //default:
            return 0;
    }
}

बस एक सरल अनुक्रमित शाखा में संकलित किया गया है:

c:
        and     edi, 15
        jmp     [QWORD PTR .L10[0+rdi*8]]
.L10:
        .quad   .L12
        .quad   .L12
        .quad   .L9
        .quad   .L9
        .quad   .L11
        .quad   .L11
        .quad   .L9
        .quad   .L9
        .quad   .L12
etc...

ध्यान दें कि यदि default:असंबद्ध है, तो gcc अपने नेस्टेड शाखा संस्करण पर वापस आ जाता है।


1
@LambdaBeta आपको मेरे जवाब को अस्वीकार करने और इसे स्वीकार करने पर विचार करना चाहिए, क्योंकि आधुनिक इंटेल सीपीयू दो समानांतर अनुक्रमित मेमोरी रीड / साइकल कर सकते हैं जबकि मेरी ट्रिक का थ्रूपुट शायद 1 लुक / साइकल है। दूसरी तरफ, शायद मेरी हैक SSE2 pslld/ psradया उनके 8-वे AVX2 समकक्षों के साथ 4-वे वेक्टराइज़ेशन के लिए अधिक उत्तरदायी है । बहुत कुछ आपके कोड की अन्य विशिष्टताओं पर निर्भर करता है।
इविलनोटिक्सिस्ट इडोनोटेक्सिस्ट

4

सी कंपाइलर्स के लिए विशेष मामले हैं switch, क्योंकि वे उम्मीद करते हैं कि प्रोग्रामर इसके मुहावरे को समझेंगे switchऔर उसका फायदा उठाएंगे ।

जैसे कोड:

if (num == 0 || num == 1 || num == 8 || num == 9) 
    return -1;

if (num == 4 || num == 5 || num == 12 || num == 13)
    return 1;

सक्षम सी कोडर्स द्वारा समीक्षा पारित नहीं किया जाएगा; तीन या चार समीक्षकों ने एक साथ "यह होना चाहिए switch!"

यह सी कंपाइलर्स के लिए ifछलांग तालिका में रूपांतरण के लिए बयानों की संरचना का विश्लेषण करने के लिए इसके लायक नहीं है । इसके लिए स्थितियां ठीक ही होनी चाहिए, और ifबयानों के एक समूह में भिन्नता की मात्रा खगोलीय है। विश्लेषण दोनों जटिल है और नकारात्मक आने की संभावना है (जैसे: "नहीं, हम इन ifएस को switch" में परिवर्तित नहीं कर सकते हैं )।


मुझे पता है, इसीलिए मैंने स्विच से शुरुआत की। हालांकि, अगर समाधान मेरे मामले में काफी तेज है। मैं मूल रूप से पूछ रहा हूं कि क्या स्विच के लिए एक बेहतर समाधान का उपयोग करने के लिए कंपाइलर को समझाने का एक तरीका है, क्योंकि यह ifs में पैटर्न खोजने में सक्षम था, लेकिन स्विच नहीं। (मैं विशेष रूप से
ifs

भावुक हो गए लेकिन भावुक नहीं हुए क्योंकि यही कारण है कि मैंने यह सवाल किया। मैं स्विच का उपयोग करना चाहता हूं , लेकिन यह मेरे मामले में बहुत धीमा है, मैं ifअगर संभव हो तो बचना चाहता हूं ।
लैंबडेबेटा

@LambdaBeta: लुकअप टेबल से बचने का कोई कारण है? यदि आप इसे निर्दिष्ट कर रहे हैं तो इसे थोड़ा और स्पष्ट करना चाहते हैं, और इसे स्पष्ट रूप से पूरी तरह से ठीक करना चाहते हैं static, तो इसे बनाएं और C99 नामित इनिशियलाइज़र का उपयोग करें
शैडो रेंजर

1
मैं कम से कम कम त्यागना शुरू करूंगा ताकि आशावादी के लिए कम काम करना पड़े।
R .. गिटहब स्टॉप हेल्पिंग ICE

@ShadowRanger दुर्भाग्य से यह अभी भी की तुलना में धीमी है if(देखें संपादित करें)। @ आर .. मैंने कंपाइलर के लिए पूर्ण बिटवाइज़ समाधान का काम किया, जो कि मैं अभी के लिए उपयोग कर रहा हूं। दुर्भाग्य से मेरे मामले में ये enumमूल्य हैं, नग्न पूर्णांक नहीं, इसलिए बिटवाइज़ हैक्स बहुत रखरखाव योग्य नहीं हैं।
लैंबडाबेटा

4

निम्न कोड आपके लुकअप ब्रांचफ्री, LUT-free, ~ 3 घड़ी चक्र, ~ 4 उपयोगी निर्देशों और ~ 13 बाइट्स को अत्यधिक- inlineable x86 मशीन कोड में गणना करेगा ।

यह 2 के पूरक पूर्णांक प्रतिनिधित्व पर निर्भर करता है।

हालाँकि, आपको यह सुनिश्चित करना चाहिए कि u32और s32टाइप किए गए अक्षर वास्तव में 32-बिट अहस्ताक्षरित और हस्ताक्षरित पूर्णांक प्रकारों को इंगित करते हैं। stdint.hप्रकार uint32_tऔर int32_tउपयुक्त होता, लेकिन हेडर आपके पास उपलब्ध होने पर मुझे कोई पता नहीं है।

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}


int d(int num){
    typedef unsigned int u32;
    typedef signed   int s32;

    // const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
    // 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
    // Hexadecimal:                   F     0     5     0     F     0     5     0
    const u32 K = 0xF050F050U;

    return (s32)(K<<(num+num)) >> 30;
}

int main(void){
    for(int i=0;i<16;i++){
        if(a(i) != d(i)){
            return !0;
        }
    }
    return 0;
}

अपने लिए यहां देखें: https://godbolt.org/z/AcJWWf


स्थिरांक के चयन पर

आपकी खोज -1 और +1 के बीच 16 बहुत छोटे स्थिरांक के लिए है। प्रत्येक 2 बिट के भीतर फिट बैठता है और उनमें से 16 हैं, जिन्हें हम निम्नानुसार रख सकते हैं:

// const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
// 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
// Hexadecimal:                   F     0     5     0     F     0     5     0
u32 K = 0xF050F050U;

इंडेक्स 0 के साथ उन्हें सबसे महत्वपूर्ण बिट के पास रखकर, एक सिंगल शिफ्ट 2*numआपके 2-बिट नंबर के साइन बिट को रजिस्टर के साइन बिट में रखेगा। 32-2 = 30 बिट्स द्वारा 2-बिट संख्या को दाईं ओर स्थानांतरित करना इसे intपूरा करता है, चाल को पूरा करता है।


यह सिर्फ सबसे अच्छा तरीका हो सकता है यह एक magicटिप्पणी के साथ यह करने के लिए कि यह कैसे पुनर्जन्म करना है। क्या आप बता सकते हैं कि आप इसके साथ कैसे आए?
लैम्ब्डाबेटा

स्वीकार किया जा सकता है क्योंकि यह 'साफ' होने के साथ-साथ तेज भी हो सकता है। (कुछ प्रीप्रोसेसर जादू के माध्यम से :) < xkcd.com/541 >)
लैंबडाटा

1
मेरे शाखाहीन प्रयास को !!(12336 & (1<<x))-!!(771 & (1<<x));
हरा देता है

0

आप केवल अंकगणित का उपयोग करके एक ही प्रभाव बना सकते हैं:

// produces : -1 -1 0 0 1 1 0 0 -1 -1 0 0 1 1 0 0 ...
int foo ( int x )
{
    return 1 - ( 3 & ( 0x46 >> ( x & 6 ) ) );
}

हालांकि, तकनीकी रूप से, यह अभी भी एक (बिटवाइज़) लुकअप है।

यदि ऊपर बहुत अधिक रहस्यमय लगता है, तो आप यह भी कर सकते हैं:

int foo ( int x )
{
    int const y = x & 6;
    return (y == 4) - !y;
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.