c - सख्त एलियासिंग नियम क्या है?




undefined-behavior c++-faq (6)

सी में सामान्य अपरिभाषित व्यवहार के बारे में पूछते समय, आत्माओं ने सख्त अलियासिंग नियम को संदर्भित करने से अधिक प्रबुद्ध किया।
उनकी बातचीत किस बारे में हो रही है?


कई उत्तरों को पढ़ने के बाद, मुझे कुछ जोड़ने की आवश्यकता महसूस होती है:

सख्त एलियासिंग (जो मैं थोड़ा सा वर्णन करूंगा) महत्वपूर्ण है क्योंकि :

  1. मेमोरी एक्सेस महंगा (प्रदर्शन के अनुसार) हो सकती है, यही कारण है कि भौतिक स्मृति पर वापस जाने से पहले सीपीयू रजिस्टरों में डेटा का उपयोग किया जाता है

  2. यदि दो अलग-अलग CPU रजिस्टरों में डेटा उसी स्मृति स्थान पर लिखा जाएगा, तो हम भविष्यवाणी नहीं कर सकते कि सी में कोड करते समय कौन सा डेटा "जीवित" रहेगा

    असेंबली में, जहां हम मैन्युअल रूप से सीपीयू रजिस्टरों की लोडिंग और अनलोडिंग कोड करते हैं, हम जानते होंगे कि कौन सा डेटा बरकरार है। लेकिन सी (शुक्र है) इस विस्तार को दूर कर देता है।

चूंकि दो पॉइंटर्स स्मृति में एक ही स्थान पर इंगित कर सकते हैं, इसके परिणामस्वरूप जटिल कोड हो सकता है जो संभव टकराव को संभालता है

यह अतिरिक्त कोड धीमा है और प्रदर्शन को नुकसान पहुंचाता है क्योंकि यह अतिरिक्त मेमोरी रीड / राइट ऑपरेशंस करता है जो धीमे और (संभवतः) दोनों अनावश्यक होते हैं।

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

सख्त एलियासिंग कहता है कि यह मानना ​​सुरक्षित है कि पॉइंटर्स विभिन्न प्रकारों को स्मृति में विभिन्न स्थानों पर इंगित करते हैं।

यदि एक कंपाइलर नोटिस करता है कि दो पॉइंटर्स विभिन्न प्रकारों (उदाहरण के लिए, एक int * और एक float * ) इंगित करते हैं, तो यह मान लेगा कि मेमोरी एड्रेस अलग है और यह मेमोरी एड्रेस टकराव के खिलाफ सुरक्षा नहीं करेगा , जिसके परिणामस्वरूप तेज मशीन कोड होगा।

उदाहरण के लिए :

आइए निम्नलिखित फ़ंक्शन मान लें:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

उस मामले को संभालने के लिए जिसमें a == b (दोनों पॉइंटर्स एक ही मेमोरी को इंगित करते हैं), हमें स्मृति से डेटा को सीपीयू रजिस्टरों तक लोड करने के तरीके को ऑर्डर करने और परीक्षण करने की आवश्यकता होती है, इसलिए कोड इस तरह समाप्त हो सकता है:

  1. स्मृति से a और b लोड करें।

  2. b जोड़ें।

  3. b बचाओ और पुनः लोड करें

    (सीपीयू रजिस्टर से मेमोरी में सहेजें और मेमोरी से सीपीयू रजिस्टर में लोड करें)।

  4. b जोड़ें।

  5. a (सीपीयू रजिस्टर से) स्मृति में सहेजें।

चरण 3 बहुत धीमा है क्योंकि इसे भौतिक स्मृति तक पहुंचने की आवश्यकता है। हालांकि, उन मामलों के खिलाफ सुरक्षा की आवश्यकता है जहां a और b उसी स्मृति पते पर इंगित करें।

सख्त एलियासिंग हमें संकलक को यह बताकर रोकने के लिए अनुमति देगा कि ये मेमोरी पते अलग-अलग हैं (जो, इस मामले में, और भी अनुकूलन की अनुमति देगा जो पॉइंटर्स मेमोरी एड्रेस साझा करते हैं)।

  1. विभिन्न प्रकारों का उपयोग करके इंगित करने के लिए इसे दो तरीकों से कंपाइलर को बताया जा सकता है। अर्थात:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. restrict कीवर्ड का उपयोग करना। अर्थात:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

अब, सख्त एलिसिंग नियम को संतुष्ट करके, चरण 3 से बचा जा सकता है और कोड काफी तेजी से चलाएगा।

वास्तव में, restrict कीवर्ड जोड़कर, संपूर्ण फ़ंक्शन को अनुकूलित किया जा सकता है:

  1. स्मृति से a और b लोड करें।

  2. b जोड़ें।

  3. दोनों को a से b तक परिणाम बचाएं।

संभवतः टकराव की वजह से यह अनुकूलन पहले नहीं किया जा सका (जहां a और b को दोगुना करने के बजाय तीन गुना किया जाएगा)।


डौग टी ने पहले ही लिखा है कि परिशिष्ट के रूप में, यहां एक साधारण परीक्षण मामला है जो शायद इसे जीसीसी के साथ ट्रिगर करता है:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

gcc -O2 -o check check.c साथ संकलित करें gcc -O2 -o check check.c . gcc -O2 -o check check.c । आमतौर पर (अधिकांश जीसीसी संस्करणों के साथ मैंने कोशिश की) यह "सख्त एलियासिंग समस्या" आउटपुट करता है, क्योंकि संकलक मानता है कि "एच" "चेक" फ़ंक्शन में "के" जैसा ही पता नहीं हो सकता है। इसके कारण संकलक if (*h == 5) दूर अनुकूलित करता है और हमेशा printf को कॉल करता है।

यहां रुचि रखने वालों के लिए x64 असेंबलर कोड है, जो जीसीसी 4.6.3 द्वारा उत्पादित है, x64 के लिए उबंटू 12.04.2 पर चल रहा है:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

तो यदि स्थिति पूरी तरह से असेंबलर कोड से चली गई है।


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

लेख से:

"सख्त एलियासिंग एक धारणा है, जिसे सी (या सी ++) कंपाइलर द्वारा बनाया गया है, कि अलग-अलग प्रकार की वस्तुओं के लिए डिफरेंसिंग पॉइंटर्स कभी भी उसी स्मृति स्थान (यानी एक दूसरे को उर्फ ​​नहीं) देखेंगे।"

तो मूल रूप से यदि आपके पास int* कुछ स्मृति की ओर इशारा करते हुए एक int* और फिर आप उस float* को उस स्मृति पर इंगित करते हैं और इसे float रूप में उपयोग करते हैं तो आप नियम तोड़ते हैं। यदि आपका कोड इसका सम्मान नहीं करता है, तो कंपाइलर का ऑप्टिमाइज़र आपके कोड को तोड़ने की संभावना है।

नियम का अपवाद एक char* , जिसे किसी भी प्रकार को इंगित करने की अनुमति है।


यह सख्त एलियासिंग नियम है, जो सी ++ 03 मानक की धारा 3.10 में पाया गया है (अन्य उत्तरों अच्छे स्पष्टीकरण प्रदान करते हैं, लेकिन कोई भी नियम स्वयं प्रदान नहीं करता है):

यदि कोई प्रोग्राम किसी ऑब्जेक्ट के संग्रहीत मूल्य को निम्न प्रकारों में से किसी एक के आभासी के माध्यम से एक्सेस करने का प्रयास करता है तो व्यवहार अपरिभाषित होता है:

  • वस्तु का गतिशील प्रकार,
  • वस्तु के गतिशील प्रकार का एक सीवी-योग्य संस्करण,
  • एक प्रकार जो ऑब्जेक्ट के गतिशील प्रकार से संबंधित हस्ताक्षरित या हस्ताक्षरित प्रकार है,
  • एक प्रकार जो ऑब्जेक्ट के गतिशील प्रकार के सीवी-योग्य संस्करण के अनुरूप हस्ताक्षरित या हस्ताक्षरित प्रकार है,
  • एक कुल या संघीय प्रकार जिसमें इसके सदस्यों के बीच उपर्युक्त प्रकारों में से एक शामिल है (समवर्ती रूप से, एक उपनिवेश या निहित संघ का सदस्य),
  • एक प्रकार जो ऑब्जेक्ट के गतिशील प्रकार के (संभवतः सीवी-योग्य) बेस क्लास प्रकार है,
  • एक char या unsigned char प्रकार।

सी ++ 11 और सी ++ 14 शब्द (परिवर्तन पर बल दिया गया):

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

  • वस्तु का गतिशील प्रकार,
  • वस्तु के गतिशील प्रकार का एक सीवी-योग्य संस्करण,
  • वस्तु के गतिशील प्रकार के लिए एक प्रकार (जैसा कि 4.4 में परिभाषित किया गया है)
  • एक प्रकार जो ऑब्जेक्ट के गतिशील प्रकार से संबंधित हस्ताक्षरित या हस्ताक्षरित प्रकार है,
  • एक प्रकार जो ऑब्जेक्ट के गतिशील प्रकार के सीवी-योग्य संस्करण के अनुरूप हस्ताक्षरित या हस्ताक्षरित प्रकार है,
  • एक समेकित या संघीय प्रकार जिसमें उपरोक्त प्रकारों में से एक तत्व या गैर स्थैतिक डेटा सदस्यों (एक पुनरावर्ती, एक तत्व या गैर-स्थैतिक डेटा सदस्य या उपनिवेश संघ) शामिल है,
  • एक प्रकार जो ऑब्जेक्ट के गतिशील प्रकार के (संभवतः सीवी-योग्य) बेस क्लास प्रकार है,
  • एक char या unsigned char प्रकार।

दो परिवर्तन छोटे थे: अंतराल के बजाय glvalue , और कुल / संघ मामले के स्पष्टीकरण।

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

इसके अलावा सी वर्डिंग (सी 99; आईएसओ / आईईसी 98 99: 1 999 6.5 / 7; सटीक वही शब्द आईएसओ / आईईसी 98 99: 2011 §6.5 ¶7 में उपयोग किया जाता है):

किसी ऑब्जेक्ट में इसके संग्रहीत मूल्य को केवल एक आभासी अभिव्यक्ति द्वारा उपयोग किया जाएगा जिसमें निम्न में से एक प्रकार 73 है) या 88) :

  • वस्तु के प्रभावी प्रकार के साथ संगत एक प्रकार,
  • ऑब्जेक्ट के प्रभावी प्रकार के साथ संगत प्रकार के योग्यता संस्करण,
  • एक प्रकार जो ऑब्जेक्ट के प्रभावी प्रकार से संबंधित हस्ताक्षरित या हस्ताक्षरित प्रकार है,
  • एक प्रकार जो ऑब्जेक्ट के प्रभावी प्रकार के क्वालीफ संस्करण के अनुरूप हस्ताक्षरित या हस्ताक्षरित प्रकार है,
  • एक कुल या संघीय प्रकार जिसमें इसके सदस्यों के बीच उपर्युक्त प्रकारों में से एक शामिल है (जिसमें, संक्षेप में, उपसमूह या निहित संघ का सदस्य शामिल है), या
  • एक चरित्र प्रकार।

73) या 88) इस सूची का उद्देश्य उन परिस्थितियों को निर्दिष्ट करना है जिनमें कोई ऑब्जेक्ट एलियाड किया जा सकता है या नहीं।


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


सख्त एलियासिंग समस्याओं का सामना करने वाली एक सामान्य स्थिति तब होती है जब आपके सिस्टम के शब्द आकार (जैसे uint32_t s या uint16_t s के पॉइंटर) पर एक संरचना (जैसे डिवाइस / नेटवर्क संदेश) को uint16_t । जब आप ऐसे बफर पर एक संरचना को ओवरले करते हैं, या पॉइंटर कास्टिंग के माध्यम से ऐसी संरचना पर एक बफर आप आसानी से सख्त एलियासिंग नियमों का उल्लंघन कर सकते हैं।

तो इस तरह के सेटअप में, अगर मैं किसी चीज़ को संदेश भेजना चाहता हूं तो मुझे दो असंगत पॉइंटर्स रखना होगा जो स्मृति के समान हिस्से को इंगित करते हैं। मैं फिर इस तरह कुछ कोड को निष्क्रिय कर सकता हूं:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

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

(जीसीसी एलियासिंग चेतावनियां देने की अपनी क्षमता में कुछ हद तक असंगत प्रतीत होता है, कभी-कभी हमें एक दोस्ताना चेतावनी देता है और कभी-कभी नहीं।)

यह व्यवहार देखने के लिए कि यह व्यवहार अपरिभाषित क्यों है, हमें इस बारे में सोचना होगा कि सख्त एलियासिंग नियम कंपाइलर को क्या खरीदता है। असल में, इस नियम के साथ, इसे लूप के प्रत्येक भाग को buff की सामग्री को रीफ्रेश करने के लिए निर्देश डालने के बारे में सोचना नहीं है। इसके बजाए, एलियासिंग के बारे में कुछ परेशान अनजान धारणाओं के साथ ऑप्टिमाइज़ करते समय, यह उन निर्देशों को छोड़ सकता है, लूप चलाने के एक बार पहले सीपीयू रजिस्टरों में लोड buff[0] और buff[1 ] लोड कर सकते हैं, और लूप के शरीर को तेज कर सकते हैं। सख्त एलियासिंग शुरू करने से पहले, कंपाइलर को परावर्तक की स्थिति में रहना पड़ा था कि buff की सामग्री किसी भी समय कहीं से भी बदल सकती है। तो एक अतिरिक्त प्रदर्शन बढ़त पाने के लिए, और मानते हैं कि ज्यादातर लोग टाइप-पन पॉइंटर्स नहीं देखते हैं, सख्त एलियासिंग नियम पेश किया गया था।

ध्यान रखें, अगर आपको लगता है कि उदाहरण बढ़ गया है, तो यह भी हो सकता है कि यदि आप किसी अन्य फ़ंक्शन को आपके लिए भेजने के लिए बफर पास कर रहे हैं, तो इसके बजाय यदि आपके पास है।

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

और इस सुविधाजनक फ़ंक्शन का लाभ उठाने के लिए हमारे पिछले लूप को फिर से लिखें

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

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

तो मैं इस के आसपास कैसे हो सकता है?

  • एक संघ का प्रयोग करें। अधिकांश कंपाइलर सख्त एलियासिंग के बारे में शिकायत किए बिना इसका समर्थन करते हैं। सी 99 में इसकी अनुमति है और स्पष्ट रूप से सी 11 में अनुमति है।

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • आप अपने कंपाइलर में सख्त एलियासिंग अक्षम कर सकते हैं ( f[no-]strict-aliasing जीसीसी में f[no-]strict-aliasing ))

  • आप अपने सिस्टम के शब्द के बजाय एलआईज़िंग के लिए char* उपयोग कर सकते हैं। नियम char* लिए अपवाद की अनुमति देते हैं ( signed char और signed char सहित)। यह हमेशा माना जाता है कि char* अन्य प्रकार उपनाम। हालांकि यह दूसरी तरफ काम नहीं करेगा: कोई धारणा नहीं है कि आपकी संरचना वर्णों का बफर उपनाम करती है।

शुरुआती सावधान रहें

एक दूसरे पर दो प्रकार के ओवरले करते समय यह केवल एक संभावित खनन क्षेत्र है। आपको endianness , शब्द संरेखण , और पैकिंग structs के माध्यम से संरेखण के मुद्दों से निपटने के तरीके के बारे में भी सीखना चाहिए।





type-punning