c सी में दोगुना करने के लिए सुरक्षित रूप से चार*को दबाना




endianness type-punning (4)

मानक कहते हैं कि एक संघ के एक क्षेत्र को लिखना और इसे तुरंत पढ़ना अनिर्धारित व्यवहार है। इसलिए यदि आप नियम पुस्तिका से जाते हैं, तो संघ आधारित विधि काम नहीं करेगी।

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

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

असल में, विभिन्न आकारों में से एक से चार में बदलना आसान है:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    return *(int64_t *) buf;
}

और मेरे पास बाइट ऑर्डर को आवश्यकतानुसार स्वैप करने के लिए समर्थन कार्यों का एक कास्ट है, जैसे:

int64_t swappedint64_t(const int64_t wrongend)
{
    /* Change the endianness of a 64-bit integer */
    return (((wrongend & 0xff00000000000000LL) >> 56) |
            ((wrongend & 0x00ff000000000000LL) >> 40) |
            ((wrongend & 0x0000ff0000000000LL) >> 24) |
            ((wrongend & 0x000000ff00000000LL) >> 8)  |
            ((wrongend & 0x00000000ff000000LL) << 8)  |
            ((wrongend & 0x0000000000ff0000LL) << 24) |
            ((wrongend & 0x000000000000ff00LL) << 40) |
            ((wrongend & 0x00000000000000ffLL) << 56));
}

रनटाइम में, प्रोग्राम मशीन की अंत्यता को पता लगाता है और एक फ़ंक्शन पॉइंटर को ऊपर दिए गए एक को निर्दिष्ट करता है:

int64_t (*slittleint64_t)(const char *);
if(littleendian) {
    slittleint64_t = snativeint64_t;
} else {
    slittleint64_t = sswappedint64_t;
}

अब, मुश्किल भाग आता है जब मैं एक डबल * करने के लिए एक डबल * डालने की कोशिश कर रहा हूँ मैं एंडियन-स्वैपिंग कोड को फिर से उपयोग करना चाहता हूं:

union 
{
    double  d;
    int64_t i;
} int64todouble;

int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);

हालांकि, कुछ कंपलर "int64todouble.i" असाइन को दूर कर सकते हैं और प्रोग्राम को तोड़ सकते हैं। ऐसा करने का एक सुरक्षित तरीका है, जबकि यह विचार करते हुए कि इस कार्यक्रम को प्रदर्शन के लिए अनुकूलित किया जाना चाहिए, और यह भी कि मैं सीधे डबल करने के लिए चार * कास्ट करने के लिए परिवर्तनों के समानांतर सेट को नहीं लिखना चाहूंगा? यदि दंड का संघ विधि सुरक्षित है, तो क्या मुझे अपने कार्यों जैसे स्नैचेंट 64_t को फिर से लिखना चाहिए?

स्टीव जेसप के उत्तर का उपयोग करते हुए मैंने समाप्त कर दिया क्योंकि रूपांतरण फ़ंक्शन को मेम्क्पी का उपयोग करने के लिए फिर से लिखे, जैसे:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    int64_t output;
    memcpy(&output, buf, 8);
    return output;
}

अपने मूल कोड के रूप में सटीक समान कोडर में संकलित:

snativeint64_t:
        movq    (%rdi), %rax
        ret

दोनों में, मेम्क्पी संस्करण अधिक स्पष्ट रूप से व्यक्त करता है कि मैं क्या करने की कोशिश कर रहा हूं और यहां तक ​​कि सबसे सरल कंपलर पर काम करना चाहिए।

एडम, आपका जवाब भी बढ़िया था और मैंने उससे बहुत कुछ सीखा। पोस्ट करने का शुक्रिया!


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

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

int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you're lucky */
printf("%lf", d);

परिणामस्वरूप कोड की जांच करें, या बस इसे प्रोफ़ाइल। संभावना सबसे खराब स्थिति में भी धीमी गति से नहीं होगी।

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

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


संपादित करें:
प्रश्नकर्ता ने किसी अन्य कार्यक्रम का उल्लेख नहीं किया है, क्योंकि डेटा को हमेशा बड़े एंडियन के रूप में प्रभावी तरीके से स्टोर करने और मशीन अंतहीनता को कैसे व्यवस्थित रखने के बारे में टिप्पणियां हटाई गईं (जो महत्वपूर्ण जानकारी है)।

फिर भी अगर डेटा को किसी एन्डियन से बड़ा और बड़े से एन्डियन होस्ट करने के लिए रूपांतरण की आवश्यकता होती है, ntohs / ntohl / htons / htonl सबसे अच्छे तरीके हैं, सबसे तेज और गति में अपराजेय (जैसा कि वे हार्डवेयर में कार्य करते हैं, यदि CPU समर्थन करता है, तो हरा नहीं जा सकता)

डबल / फ्लोट के बारे में, बस उन्हें मेमोरी कास्टिंग द्वारा इनट में रखें:

double d = 3.1234;
printf("Double %f\n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f\n", d2);

इसे फ़ंक्शन में लपेटें

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

double int64ToDouble(int64_t i)
{
    return *(double *)&i;
}

प्रश्नकर्ता ने यह लिंक प्रदान किया:

http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

जैसा साबित करता है कि कास्टिंग खराब है ... दुर्भाग्य से मैं केवल इस पृष्ठ के अधिकांश के साथ दृढ़ता से असहमत हूं। उद्धरण और टिप्पणियां:

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

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

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

यह सच है, लेकिन दुर्भाग्य से पूरी तरह से मेरे कोड से असंबंधित है

क्या वह इस तरह से कोड को संदर्भित करता है:

int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;

अब दोहरे सूचक और intPointer दोनों एक ही स्मृति स्थान पर इंगित करते हैं, लेकिन यह एक ही प्रकार के रूप में इलाज करते हैं। यह ऐसी स्थिति है जिसे आप संघ के साथ हल करना चाहिए, कुछ और बहुत खराब है बुरा है कि मेरा कोड क्या नहीं है!

मेरी कोड की प्रतियां मूल्य से , नहीं संदर्भ के द्वारा मैं इंट 64 पॉइंटर (या दूसरी तरफ गोल) के लिए एक दोगुना डालता हूं और तुरंत इसे ध्यान में रखता हूं। कार्यों को वापस आने के बाद, कोई भी सूचक कोई भी नहीं होता है। वहाँ एक int64 और एक डबल है और ये पूरी तरह से कार्यों के इनपुट पैरामीटर से असंबंधित हैं। मैंने कभी भी किसी अन्य सूचक के किसी सूचक को प्रतिलिपि नहीं किया है (यदि आपने इसे अपने कोड नमूने में देखा था, तो आपने सी कोड लिखा है जिसे मैंने काफी ग़लत तरीके से पढ़ा है), मैं सिर्फ वैल्यू को एक अलग प्रकार के वेरिएबल (खुद की स्मृति स्थान) में स्थानांतरित करता हूं । इसलिए टाइपिंग की परिभाषा बिल्कुल भी लागू नहीं होती है, क्योंकि यह कहती है "स्मृति में एक ही स्थान का संदर्भ लें" और यहाँ कुछ भी नहीं है उसी स्मृति स्थान को संदर्भित करता है।

int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;

मेरा कोड किसी मेमोरी कॉपी की तुलना में कुछ और नहीं है, जो सिर्फ बाहरी फ़ंक्शन के बिना सी में लिखा गया है।

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

के रूप में लिखा जा सकता है

int64_t doubleToInt64(double d)
{
    int64_t result;
    memcpy(&result, &d, sizeof(d));
    return result;
}

यह उस से ज्यादा कुछ नहीं है, इसलिए कहीं भी दृष्टि में कोई प्रकार का दंड नहीं है। और यह ऑपरेशन पूरी तरह से सुरक्षित है, क्योंकि ऑपरेशन के रूप में सुरक्षित सी में हो सकता है। डबल को हमेशा 64 बिट के लिए परिभाषित किया जाता है (कोई अंतर नहीं, यह आकार में भिन्न नहीं होता है, यह 64 बिट पर तय होता है), इसलिए यह हमेशा फिट होगा एक int64_t आकार के चर में


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





type-punning