در علوم کامپیوتر به هر تکنیکی که باعث دور زدن یا از کار انداختن Type System یک زبان برنامه نویسی شود Type Punning میگویند. معمولا از این روش برای به دست آوردن تاثیری استفاده میشود که حصول آن با ابزار رسمی موجود در حوزه یک زبان یا سخت است یا غیر ممکن.
در واقع با type punning بخشی از حافظه که با یک نوع خاص تفسیر میشود را با نوع دیگر تفسیر میکنیم. این کار زمانی مفید است که میخواهیم به بازنمایی زیرین اشیا دسترسی پیدا کنیم و آن را مشاهده یا دستکاری کنیم و یا انتقال دهیم. این عمل معمولا در کدهای مربوط به serialization و شبکه بیشتر مشاهده میشوند.
در زبانهای C و ++C ساختارهایی مثل تبدیل نوع اشارهگر و union و به طور خاص در ++C تبدیل نوع ارجاع ها و عملگر reinterpret_cast
موجود هستند که اجازه انجام type punning های مختلف را میدهند، اگرچه برخی از این روشها توسط استاندارهای زبان پشتیبانی نمیشوند.
به طور سنتی این کار معمولا با گرفتن آدرس یک شی و تبدیل اشاره گر آن به اشاره گر نوع دیگری که میخواهیم به آن تفسیر کنیم آغاز میشود و در نهایت با استفاده از اشاره گر جدید به محتوای آن دسترسی پیدا میکنیم.
در زیر مثال ساده ای از type punning را میبینیم:
int numberInt = 42; short *numberShort = (short *) &numberInt ; (*numberShort) = 43;
مثال دیگری که معمولا برای بررسی منفی بودن یک متغیر float استفاده میشود به طوری که برای سرعت بیشتر از عملگرهای fpu استفاده نشود به صورت زیر است:
bool is_negative(float x) { return x < 0.0; }
با توجه به فرض این که مقایسه اعداد ممیز شناور پر هزینه است و همچنین با فرض بازنمایی اعداد float به صورت استاندارد IEEE 754 و ۳۲ بیت بودن طول اعداد integer میتوانیم با استخراج کردن بیت علامت از عدد ممیز شناور به منفی یا مثبت بودن آن پی ببریم:
bool is_negative(float x) { unsigned int *ui = (unsigned int *)&x; return *ui & 0x80000000; }
همچنین از Union نیز میتوان برای type punning استفاده کرد:
bool is_negative(float x) { union { unsigned int ui; float d; } my_union = { .d = x }; return my_union.ui & 0x80000000; }
اگر در استفاده از روشهای type punning دقت نشود باعث ایجاد Pointer aliasing و نقض قانون strict aliasing شده و باعث ایجاد رفتار تعریف نشده (undefined behavior) میشود.
در برنامه نویسی aliasing به معنای این است که به یک مکان حافظه مشخص با استفاده از دو نام مختلف دسترسی پیدا کرد به طول مثال:
int anint; int *intptr = &anint;
اگر مقدار *intptr را تغییر دهیم مقداری که توسط anint مشخص میشود هم تغییر خواهد کرد، در اینجا intptr نام دیگری است برای یک چیز مشخص.
به این معناست که dereference کردن اشاره گرها به اشیایی با انواع متفاوت نباید منجر به ارجاع به مکان یکسانی از حافظه شود (یعنی نباید همدیگر را alias کنند).
وجود aliasing باعث ایجاد محدودیت های سختی روی ترتیب اجرای دستورالعملهای برنامه میشود. اگر متن برنامه نوشته شده دو عمل نوشتن روی متغیرهایی که همدیگر را alias کردهاند وجود داشته باشد آن دستورات باید عینا و به همان ترتیب در کد ماشین تولید شده قرار گیرند. اصولا هرگونه جابه جایی دستورالعمل های خواندن و نوشتن چنین متغیرهایی باعث تولید نتیجه اشتباه در برنامه خواهد شد.
به طور مثال تابع زیر را در نظر بگیرید:
void updatePtrs(size_t *ptrA, size_t *ptrB, size_t *val) { *ptrA += *val; *ptrB += *val; }
این تابع را می توان طوری فراخوانی کرد که اشاره گرهای ptrA و ptrB هر دو به یک مکان حافظه اشاره کنند:
size_t val = 5; size_t sizeVar = 10; updatePtrs(&sizeVar, &SizeVar, &val);
در این صورت کد assembly زیر میتواند تولید شود:
; Hypothetical RISC Machine. ldr r12, [val] ; Load memory at val to r12. ldr r3, [ptrA] ; Load memory at ptrA to r3. add r3, r3, r12 ; Perform addition: r3 = r3 + r12. str r3, [ptrA] ; Store r3 to memory location ptrA, updating the value. ldr r3, [ptrB] ; 'load' may have to wait until preceding 'store' completes. ldr r12, [val] ; Have to load a second time to ensure consistency. add r3, r3, r12 str r3, [ptrB]
در این کد ابتدا مقدار val درون رجیستر r12 لود میشود و بعد مقدار ptrA در ریجستر r3 لود شده و سپس با هم جمع میشوند و مقدار در prtA ذخیره میشود. همین عمل برای ptrB هم انجام میشود. که در نهایت مقدار sizeVar مساوی 20 میشود.
اما به علت وجود قانون strict aliasing بخش بهینه ساز(optimizer) کامپایلر میتواند برای تولید کد بهینه ترتیب برخی دستور العمل ها را عوض کند به طور مثال برای تابع بالا کد زیر تولید خواهد شد:
ldr r12, [val] ; Note that val is now only loaded once. ldr r3, [ptrA] ; Also, all 'load's in the beginning ... ldr r4, [ptrB] add r3, r3, r12 add r4, r4, r12 str r3, [ptrA] ; ... all 'store's in the end. str r4, [ptrB]
در این کد بهینه ساز ابتدا همه مقدار را در رجیسترها لود میکند و بعد عمل جمع و سپس ذخیره سازی را انجام میدهد. که در نهایت مقدار sizeVar مساوی 15میشود که با نتیجه حالت قبلا کاملا متفاوت و نادرست است.
راه صحیح type punning در زبانهای C و ++C استفاده از تابع memcpy است که به نظر کمی سنگین میرسد اما در زمان کامپایل بهینه ساز باید موارد استفاده این تابع را برای type punning تشخیص دهد و آنها را بهینه کنه.
https://en.wikipedia.org/wiki/Type_punning
https://en.wikipedia.org/wiki/Pointer_aliasing
https://en.wikipedia.org/wiki/Restrict
https://gist.github.com/shafik/848ae25ee209f698763cffee272a58f8
https://blog.regehr.org/archives/1307