เมื่อถามถึงพฤติกรรมที่ไม่ได้กำหนดร่วมกันใน Cบางครั้งผู้คนอ้างถึงกฎนามแฝงที่เข้มงวด
พวกเขากำลังพูดเกี่ยวกับอะไร?
เมื่อถามถึงพฤติกรรมที่ไม่ได้กำหนดร่วมกันใน Cบางครั้งผู้คนอ้างถึงกฎนามแฝงที่เข้มงวด
พวกเขากำลังพูดเกี่ยวกับอะไร?
คำตอบ:
สถานการณ์ทั่วไปที่คุณพบปัญหานามแฝงที่เข้มงวดคือเมื่อวางทับโครงสร้าง (เช่นอุปกรณ์ / เครือข่าย msg) ลงบนบัฟเฟอร์ของขนาดคำของระบบของคุณ (เช่นตัวชี้ไปยังuint32_t
s หรือuint16_t
s) เมื่อคุณวางทับโครงสร้างลงบนบัฟเฟอร์หรือบัฟเฟอร์ลงบนโครงสร้างผ่านการชี้ตัวชี้คุณสามารถละเมิดกฎนามแฝงที่เข้มงวดได้อย่างง่ายดาย
ดังนั้นในการตั้งค่าแบบนี้ถ้าฉันต้องการส่งข้อความถึงสิ่งที่ฉันจะต้องมีพอยน์เตอร์ที่เข้ากันไม่ได้สองตัวชี้ไปที่หน่วยความจำอันเดียวกัน ฉันอาจไร้เดียงสารหัสเช่นนี้ (ในระบบด้วยsizeof(int) == 2
):
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]);
}
}
กฎนามแฝงที่เข้มงวดทำให้การตั้งค่านี้ผิดกฎหมาย: การยกเลิกการอ้างอิงตัวชี้ที่ทำให้นามแฝงวัตถุที่ไม่ใช่ประเภทที่เข้ากันได้หรือประเภทอื่นที่อนุญาตโดย C 2011 6.5 วรรค 7 1เป็นพฤติกรรมที่ไม่ได้กำหนด น่าเสียดายที่คุณยังคงสามารถเขียนโค้ดได้ด้วยวิธีนี้อาจจะได้รับคำเตือนบางอย่างรวบรวมได้ดีเพื่อให้มีพฤติกรรมที่ไม่คาดคิดแปลก ๆ เมื่อคุณเรียกใช้รหัส
(GCC ค่อนข้างไม่สอดคล้องกันในความสามารถในการให้คำเตือนนามแฝงบางครั้งทำให้เรามีคำเตือนที่เป็นมิตรและบางครั้งก็ไม่เป็นเช่นนั้น)
ในการดูว่าเหตุใดพฤติกรรมนี้จึงไม่ได้กำหนดเราต้องคิดว่ากฎนามแฝงที่เข้มงวดจะซื้อคอมไพเลอร์อย่างไร โดยพื้นฐานแล้วกฎนี้ไม่ต้องคิดเกี่ยวกับการแทรกคำแนะนำเพื่อรีเฟรชเนื้อหาbuff
ของการวนซ้ำทุกครั้ง แต่เมื่อปรับให้เหมาะสมด้วยข้อสันนิษฐานที่ไม่น่ารำคาญบางอย่างเกี่ยวกับนามแฝงมันสามารถตัดคำสั่งโหลดbuff[0]
และbuff[1
] ลงใน CPU register ก่อนที่จะรันลูปและเพิ่มความเร็วของลูป ก่อนที่จะใช้นามแฝงที่เข้มงวดผู้แปลต้องอาศัยอยู่ในสถานะของความหวาดระแวงว่าเนื้อหาของทุกคน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
เป็นส่วนหนึ่งของ API อื่นที่รวบรวมแยกกันก็อาจมีคำแนะนำในการโหลดเนื้อหาของบัฟ จากนั้นอีกครั้งบางทีคุณอยู่ใน C ++ และนี่คือส่วนหัวเทมเพลตบางส่วนเท่านั้นที่ใช้งานคอมไพเลอร์คิดว่ามันสามารถอินไลน์ได้ หรืออาจเป็นเพียงสิ่งที่คุณเขียนในไฟล์. c เพื่อความสะดวกของคุณ อย่างไรก็ตามพฤติกรรมที่ไม่ได้กำหนดอาจยังคงเกิดตามมา แม้ว่าเราจะรู้ว่าเกิดอะไรขึ้นภายใต้ประทุนก็ยังคงเป็นการละเมิดกฎดังนั้นจึงไม่มีการรับประกันพฤติกรรมที่ชัดเจน ดังนั้นเพียงแค่ห่อในฟังก์ชั่นที่รับบัฟเฟอร์ที่คั่นด้วยคำของเราไม่ได้ช่วยอะไรเลย
ดังนั้นฉันจะได้รับรอบนี้ได้อย่างไร
ใช้สหภาพ คอมไพเลอร์ส่วนใหญ่สนับสนุนสิ่งนี้โดยไม่บ่นเรื่องนามแฝงที่เข้มงวด สิ่งนี้ได้รับอนุญาตใน C99 และอนุญาตอย่างชัดเจนใน C11
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
คุณสามารถปิดใช้งาน aliasing ที่เข้มงวดในคอมไพเลอร์ของคุณ ( f [no-] aliasing ที่เข้มงวดใน gcc)
คุณสามารถใช้char*
นามแฝงแทนคำของระบบของคุณ กฎอนุญาตข้อยกเว้นสำหรับchar*
(รวมถึงsigned char
และunsigned char
) สันนิษฐานว่าเป็นchar*
ชื่อแทนประเภทอื่นเสมอ อย่างไรก็ตามวิธีนี้ใช้ไม่ได้ผล: ไม่มีข้อสันนิษฐานว่าโครงสร้างนามแฝงของบัฟเฟอร์
ระวังมือใหม่
นี่เป็นเขตที่วางทุ่นระเบิดที่มีศักยภาพเพียงแหล่งเดียวเมื่อทำการซ้อนทับสองประเภทเข้าด้วยกัน นอกจากนี้คุณควรเรียนรู้เกี่ยวกับendianness , การจัดเรียงคำและวิธีการจัดการกับปัญหาการจัดตำแหน่งผ่านstructs บรรจุอย่างถูกต้อง
1ประเภทที่ C 2011 6.5 7 อนุญาตให้เข้าถึง lvalue ได้:
unsigned char*
จะใช้ไกลมากchar*
แทนไหม? ฉันมักจะใช้unsigned char
แทนที่จะchar
เป็นประเภทพื้นฐานbyte
เพราะไบต์ของฉันไม่ได้ลงนามและฉันไม่ต้องการความแปลกประหลาดของพฤติกรรมที่ลงชื่อ (โดยเฉพาะอย่างยิ่ง WRT เพื่อล้น)
unsigned char *
ก็โอเค
uint32_t* buff = malloc(sizeof(Msg));
การunsigned int asBuffer[sizeof(Msg)];
ประกาศสหภาพบัฟเฟอร์ของคุณและภายหลังจะมีขนาดแตกต่างกันและไม่ถูกต้อง การmalloc
เรียกใช้การจัดตำแหน่งแบบ 4 ไบต์ภายใต้ประทุน (ไม่ต้องทำ) และการรวมจะยิ่งใหญ่กว่าที่จำเป็นต้องเป็น 4 เท่า ... ฉันเข้าใจว่าเป็นความชัดเจน แต่ไม่มีข้อผิดพลาดสำหรับฉัน น้อยลง ...
คำอธิบายที่ดีที่สุดที่ฉันได้พบคือโดยไมค์แอคตันเข้าใจแฝงเข้มงวด มันมุ่งเน้นไปที่การพัฒนา PS3 เพียงเล็กน้อย แต่โดยทั่วไปเป็นเพียง GCC
จากบทความ:
"Strict aliasing เป็นข้อสันนิษฐานที่สร้างขึ้นโดยคอมไพเลอร์ C (หรือ C ++) ที่การยกเลิกการชี้พอยน์เตอร์ไปยังวัตถุประเภทต่าง ๆ จะไม่อ้างถึงตำแหน่งหน่วยความจำเดียวกัน (เช่นนามแฝงซึ่งกันและกัน)"
ดังนั้นโดยทั่วไปถ้าคุณมีการint*
ชี้ไปยังหน่วยความจำที่มีint
และจากนั้นคุณชี้float*
ไปที่หน่วยความจำนั้นและใช้เป็นfloat
คุณทำลายกฎ หากรหัสของคุณไม่เคารพสิ่งนี้ตัวเพิ่มประสิทธิภาพของคอมไพเลอร์จะทำลายรหัสของคุณมากที่สุด
ข้อยกเว้นของกฎคือ a char*
ซึ่งได้รับอนุญาตให้ชี้ไปที่ประเภทใดก็ได้
นี่เป็นกฎนามแฝงที่เข้มงวดซึ่งพบในส่วน 3.10 ของมาตรฐานC ++ 03 (คำตอบอื่น ๆ ให้คำอธิบายที่ดี แต่ไม่มีใครให้กฎเอง):
หากโปรแกรมพยายามเข้าถึงค่าที่เก็บไว้ของวัตถุผ่าน lvalue นอกเหนือจากประเภทใดประเภทหนึ่งต่อไปนี้พฤติกรรมจะไม่ได้กำหนด:
- วัตถุประเภทไดนามิก
- รุ่นที่ผ่านการรับรอง cv ของชนิดไดนามิกของวัตถุ
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับประเภทแบบไดนามิกของวัตถุ
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับรุ่นที่ผ่านการรับรอง cv ของประเภทแบบไดนามิกของวัตถุ
- ประเภทรวมหรือสหภาพที่มีหนึ่งในประเภทดังกล่าวในหมู่สมาชิก (รวมถึง recursively เป็นสมาชิกของกลุ่มย่อยหรือสหภาพที่มี)
- ประเภทที่เป็นประเภทคลาสพื้นฐาน (อาจมีคุณสมบัติเป็น cv) ของชนิดไดนามิกของวัตถุ
char
หรือunsigned char
ประเภท
ถ้อยคำC ++ 11และC ++ 14 (เน้นการเปลี่ยนแปลง):
หากโปรแกรมพยายามเข้าถึงค่าที่เก็บไว้ของวัตถุผ่านglvalueนอกเหนือจากประเภทใดประเภทหนึ่งต่อไปนี้พฤติกรรมจะไม่ได้กำหนด:
- วัตถุประเภทไดนามิก
- รุ่นที่ผ่านการรับรอง cv ของชนิดไดนามิกของวัตถุ
- ชนิดที่คล้ายกัน (ตามที่กำหนดใน 4.4) กับชนิดไดนามิกของวัตถุ
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับประเภทแบบไดนามิกของวัตถุ
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับรุ่นที่ผ่านการรับรอง cv ของประเภทแบบไดนามิกของวัตถุ
- ชนิดรวมหรือสหภาพที่มีหนึ่งในประเภทดังกล่าวข้างต้นในองค์ประกอบหรือสมาชิกข้อมูลที่ไม่คงที่ (รวมถึงแบบเรียกซ้ำองค์ประกอบหรือสมาชิกข้อมูลไม่คงที่ของสหภาพย่อยหรือมีสหภาพ)
- ประเภทที่เป็นประเภทคลาสพื้นฐาน (อาจมีคุณสมบัติเป็น cv) ของชนิดไดนามิกของวัตถุ
char
หรือunsigned char
ประเภท
การเปลี่ยนแปลงสองอย่างมีขนาดเล็ก: glvalueแทนที่จะเป็นlvalueและการชี้แจงกรณีรวม / สหภาพ
การเปลี่ยนแปลงครั้งที่สามทำให้การรับประกันแข็งแกร่งขึ้น (ผ่อนคลายกฎนามแฝงที่แข็งแกร่ง): แนวคิดใหม่ของประเภทที่คล้ายกันซึ่งขณะนี้ปลอดภัยต่อนามแฝงแล้ว
นอกจากนี้ถ้อยคำC (C99; ISO / IEC 9899: 1999 6.5 / 7; ใช้ถ้อยคำเดียวกันนี้ใน ISO / IEC 9899: 2011 §6.5¶7):
วัตถุต้องมีค่าที่เก็บไว้เข้าถึงได้โดยนิพจน์ lvalue ที่มีประเภทใดประเภทหนึ่งต่อไปนี้73) หรือ 88) :
- ประเภทที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ
- รุ่นที่มีคุณภาพของชนิดที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับประเภทที่มีประสิทธิภาพของวัตถุ
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามซึ่งสอดคล้องกับรุ่นที่มีประสิทธิภาพของวัตถุที่มีประสิทธิภาพ
- ประเภทรวมหรือสหภาพที่มีหนึ่งในประเภทดังกล่าวในหมู่สมาชิก (รวมถึง recursively เป็นสมาชิกของกลุ่มย่อยหรือสหภาพที่มีอยู่) หรือ
- ประเภทตัวละคร
73) หรือ 88)ความตั้งใจของรายการนี้คือการระบุสถานการณ์เหล่านั้นที่วัตถุอาจหรือไม่ได้รับนามแฝง
wow(&u->s1,&u->s2)
จะต้องถูกต้องตามกฎหมายแม้ในขณะที่ตัวชี้จะใช้ในการปรับเปลี่ยนu
และที่จะปฏิเสธการเพิ่มประสิทธิภาพมากที่สุดว่า กฎนามแฝงถูกออกแบบมาเพื่ออำนวยความสะดวก
นี่เป็นข้อความที่ตัดตอนมาจาก"กฎนามแฝงที่เข้มงวดของฉันคืออะไรและทำไมเราจึงสนใจ" เขียน.
ในการสร้างสมนาม C และ C ++ นั้นเกี่ยวข้องกับประเภทนิพจน์ที่เราได้รับอนุญาตให้เข้าถึงค่าที่เก็บไว้ผ่าน ในทั้ง C และ C ++ มาตรฐานจะระบุประเภทการแสดงออกที่อนุญาตให้นามแฝงประเภทใด คอมไพเลอร์และเพิ่มประสิทธิภาพได้รับอนุญาตให้ถือว่าเราปฏิบัติตามกฎอย่างเคร่งครัด aliasing จึงระยะกฎ aliasing เข้มงวด หากเราพยายามเข้าถึงค่าโดยใช้ประเภทที่ไม่ได้รับอนุญาตจะถูกจัดประเภทเป็นพฤติกรรมที่ไม่ได้กำหนด ( UB ) เมื่อเรามีพฤติกรรมที่ไม่ได้กำหนดการเดิมพันทั้งหมดจะปิดลงผลลัพธ์ของโปรแกรมของเราจะไม่น่าเชื่อถืออีกต่อไป
น่าเสียดายที่มีการละเมิดนามแฝงที่เข้มงวดเรามักจะได้รับผลลัพธ์ตามที่เราคาดหวังไว้ซึ่งความเป็นไปได้ที่คอมไพเลอร์เวอร์ชันอนาคตที่มีการเพิ่มประสิทธิภาพใหม่จะทำลายรหัสที่เราคิดว่าถูกต้อง นี่เป็นสิ่งที่ไม่พึงปรารถนาและเป็นเป้าหมายที่คุ้มค่าที่จะเข้าใจกฎนามแฝงที่เข้มงวดและวิธีหลีกเลี่ยงการละเมิดกฎเหล่านั้น
เพื่อทำความเข้าใจเพิ่มเติมเกี่ยวกับสาเหตุที่เราสนใจเราจะหารือเกี่ยวกับปัญหาที่เกิดขึ้นเมื่อละเมิดกฎนามแฝงที่เข้มงวดพิมพ์ punning เนื่องจากเทคนิคทั่วไปที่ใช้ในประเภท punning มักจะละเมิดกฎนามแฝงที่เข้มงวดและวิธีพิมพ์ pun อย่างถูกต้อง
ลองดูตัวอย่างจากนั้นเราสามารถพูดถึงสิ่งที่มาตรฐานพูดตรวจสอบตัวอย่างเพิ่มเติมแล้วดูวิธีหลีกเลี่ยงการใช้นามแฝงที่เข้มงวดและตรวจจับการละเมิดที่เราพลาดไป นี่คือตัวอย่างที่ไม่ควรแปลกใจ ( ตัวอย่างสด ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
เรามีint * ที่ชี้ไปยังหน่วยความจำที่มีอยู่ในintและนี่คือนามแฝงที่ถูกต้อง เพิ่มประสิทธิภาพต้องคิดว่าการมอบหมายงานผ่านIPสามารถปรับปรุงค่าที่ถูกครอบครองโดยx
ตัวอย่างถัดไปแสดงนามแฝงที่นำไปสู่พฤติกรรมที่ไม่ได้กำหนด ( ตัวอย่างสด ):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
ในฟังก์ชั่นfooเราใช้int *และลอย *ในตัวอย่างนี้เราเรียกfooและตั้งค่าพารามิเตอร์ทั้งชี้ไปที่สถานที่ตั้งหน่วยความจำเดียวกันซึ่งในตัวอย่างนี้มีint หมายเหตุreinterpret_castกำลังบอกคอมไพเลอร์ให้ดำเนินการกับนิพจน์ราวกับว่ามีชนิดที่ระบุโดยพารามิเตอร์เทมเพลต ในกรณีนี้เราจะบอกว่ามันจะรักษาการแสดงออกและ xราวกับว่ามันมีชนิดลอย * เราอย่างไร้เดียงสาอาจคาดหวังผลมาจากการที่สองศาลจะเป็น0แต่เปิดใช้งานด้วยการเพิ่มประสิทธิภาพการใช้-O2ทั้ง GCC และเสียงดังกราวผลิตผลต่อไปนี้:
0
1
ซึ่งอาจไม่ได้รับการคาดหวัง แต่ใช้ได้อย่างสมบูรณ์เนื่องจากเราได้เรียกใช้พฤติกรรมที่ไม่ได้กำหนด การลอยไม่สามารถถูกต้องนามแฝงวัตถุint ดังนั้นออพติไมเซอร์สามารถสมมติค่าคงที่ 1 ที่เก็บไว้เมื่อ dereferencing iจะเป็นค่าส่งคืนเนื่องจากการจัดเก็บผ่านfไม่สามารถส่งผลกระทบต่อวัตถุintได้อย่างถูกต้อง การเสียบรหัสในคอมไพเลอร์ Explorer จะแสดงให้เห็นว่านี่เป็นสิ่งที่เกิดขึ้นจริง ( ตัวอย่างสด ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
เครื่องมือเพิ่มประสิทธิภาพที่ใช้การวิเคราะห์นามแฝงตามประเภท (TBAA)ถือว่า1จะถูกส่งคืนและย้ายค่าคงที่ไปยัง register eaxโดยตรงซึ่งมีค่าส่งคืนโดยตรง TBAA ใช้กฎภาษาเกี่ยวกับประเภทที่ได้รับอนุญาตให้นามแฝงเพื่อเพิ่มประสิทธิภาพการโหลดและร้านค้า ในกรณีนี้ TBAA รู้ว่าการลอยไม่สามารถใช้นามแฝงและintและปรับการโหลดของi ให้เหมาะสม
มาตรฐานบอกอะไรเราว่าได้รับอนุญาตและไม่ได้รับอนุญาตให้ทำ? ภาษามาตรฐานไม่ตรงไปตรงมาดังนั้นสำหรับแต่ละรายการฉันจะพยายามให้ตัวอย่างรหัสที่แสดงให้เห็นถึงความหมาย
C11มาตรฐานกล่าวว่าต่อไปนี้ในส่วน6.5 นิพจน์วรรค 7 :
วัตถุต้องมีค่าที่เก็บไว้เข้าถึงได้โดยนิพจน์ lvalue ที่มีประเภทใดประเภทหนึ่งต่อไปนี้: 88) - ชนิดที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- รุ่นที่ผ่านการรับรองของชนิดที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามซึ่งสอดคล้องกับประเภทของวัตถุที่มีประสิทธิภาพ
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang มีส่วนขยายและยังอนุญาตให้กำหนดint * ที่ไม่ได้ลงนามถึงint *แม้ว่าจะไม่ใช่ประเภทที่เข้ากันได้
- ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามซึ่งตรงกับรุ่นที่ผ่านการรับรองของประเภทที่มีประสิทธิภาพของวัตถุ
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- ประเภทรวมหรือสหภาพที่รวมประเภทหนึ่งดังกล่าวไว้ในหมู่สมาชิก (รวมถึงเรียกซ้ำสมาชิกของกลุ่มย่อยหรือสหภาพที่มีอยู่) หรือ
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- ประเภทตัวละคร
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
มาตรฐานฉบับร่าง C ++ 17 ในส่วน[basic.lval] วรรค 11พูดว่า:
หากโปรแกรมพยายามเข้าถึงค่าที่เก็บไว้ของวัตถุผ่าน glvalue นอกเหนือจากประเภทใดประเภทหนึ่งต่อไปนี้พฤติกรรมจะไม่ได้กำหนด: 63 (11.1) - ประเภทของวัตถุแบบไดนามิก
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - เวอร์ชันที่ผ่านการรับรอง cv ของชนิดไดนามิกของวัตถุ
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - ชนิดที่คล้ายกัน (ตามที่กำหนดไว้ใน 7.5) กับชนิดไดนามิกของวัตถุ
(11.4) - ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับประเภทไดนามิกของวัตถุ
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับรุ่นที่ผ่านการรับรอง cv ของประเภทแบบไดนามิกของวัตถุ
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - ชนิดรวมหรือยูเนี่ยนที่มีหนึ่งในประเภทดังกล่าวในหมู่องค์ประกอบหรือสมาชิกข้อมูลที่ไม่อยู่นิ่ง (รวมถึงแบบเรียกซ้ำองค์ประกอบหรือสมาชิกข้อมูลที่ไม่คงที่ของสหภาพย่อยหรือมีสหภาพ)
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - ประเภทที่เป็นประเภทคลาสพื้นฐาน (อาจมีคุณสมบัติเป็น cv) ของชนิดไดนามิกของวัตถุ
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - ประเภทถ่าน, ถ่านที่ไม่ได้ลงชื่อหรือ std :: byte
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
มูลค่า noting ลงนามถ่านไม่รวมอยู่ในรายการข้างต้นนี้เป็นความแตกต่างที่โดดเด่นจากCซึ่งบอกว่าเป็นชนิดตัวอักษร
เราได้มาถึงจุดนี้และเราอาจสงสัยว่าทำไมเราต้องการนามแฝง? คำตอบคือพิมพ์คำพิพากษาบ่อยครั้งวิธีที่ใช้ละเมิดกฎนามแฝงที่เข้มงวด
บางครั้งเราต้องการหลีกเลี่ยงระบบชนิดและตีความวัตถุเป็นชนิดอื่น สิ่งนี้เรียกว่าประเภท punningเพื่อตีความเซ็กเมนต์ของหน่วยความจำเป็นประเภทอื่น ประเภท punningมีประโยชน์สำหรับงานที่ต้องการเข้าถึงการเป็นตัวแทนของวัตถุเพื่อดูการขนส่งหรือจัดการ พื้นที่ทั่วไปที่เราพบว่ามีการใช้งานการสะกดคำแบบพิมพ์คือคอมไพเลอร์การทำให้เป็นอนุกรมรหัสเครือข่าย ฯลฯ ...
ตามเนื้อผ้าสิ่งนี้สามารถทำได้โดยการนำที่อยู่ของวัตถุชี้ไปที่ตัวชี้ประเภทที่เราต้องการตีความอีกครั้งเป็นแล้วการเข้าถึงค่าหรือในคำอื่น ๆ โดยนามแฝง ตัวอย่างเช่น:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
ดังที่เราได้เห็นก่อนหน้านี้นี่ไม่ใช่นามแฝงที่ถูกต้องดังนั้นเราจึงเรียกใช้พฤติกรรมที่ไม่ได้กำหนด แต่คอมไพเลอร์แบบดั้งเดิมไม่ได้ใช้ประโยชน์จากกฎนามแฝงที่เข้มงวดและรหัสประเภทนี้มักจะใช้งานได้นักพัฒนามักจะคุ้นเคยกับการทำสิ่งต่าง ๆ ด้วยวิธีนี้ วิธีสำรองทั่วไปสำหรับประเภทการติดตามคือการผ่านยูเนี่ยนซึ่งใช้ได้ใน C แต่พฤติกรรมที่ไม่ได้กำหนดใน C ++ ( ดูตัวอย่างสด ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
สิ่งนี้ไม่ถูกต้องใน C ++ และบางคนพิจารณาจุดประสงค์ของสหภาพว่าจะใช้สำหรับการใช้งานประเภทตัวแปรและรู้สึกว่าใช้สหภาพสำหรับประเภท punning เป็นการละเมิด
วิธีการมาตรฐานสำหรับประเภทเล่นสำนวนทั้ง C และ C ++ เป็นmemcpy สิ่งนี้อาจดูถนัดมือเล็กน้อย แต่เครื่องมือเพิ่มประสิทธิภาพควรจดจำการใช้memcpyสำหรับประเภท punningและปรับให้เหมาะสมและสร้างการลงทะเบียนเพื่อย้ายการลงทะเบียน ตัวอย่างเช่นถ้าเรารู้ว่าint64_tมีขนาดเท่ากับdouble :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
เราสามารถใช้memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
ในระดับที่เพิ่มประสิทธิภาพเพียงพอใด ๆ คอมไพเลอร์ที่ทันสมัยที่ดีสร้างรหัสเหมือนกันกับที่กล่าวถึงก่อนหน้านี้reinterpret_castวิธีการหรือสหภาพวิธีการสำหรับประเภทเล่นสำนวน ตรวจสอบโค้ดที่สร้างขึ้นที่เราเห็นว่าใช้เพียงลงทะเบียน mov ( ตัวอย่าง Live Compiler Explorer )
ใน C ++ 20 เราอาจได้รับbit_cast ( การนำไปใช้ในลิงก์จากข้อเสนอ ) ซึ่งเป็นวิธีที่ง่ายและปลอดภัยในการพิมพ์-Pun รวมถึงการใช้งานในบริบทของกลุ่ม
ต่อไปนี้เป็นตัวอย่างของวิธีการใช้bit_castเพื่อพิมพ์ pun int ที่ไม่ได้ลงนามเพื่อลอย ( ดูสด ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
ในกรณีที่ประเภทถึงและจากไม่มีขนาดเดียวกันเราต้องใช้โครงสร้างระดับกลาง 15 เราจะใช้ struct ที่มีอาร์เรย์อักขระsizeof (unsigned int) ( ถือว่า 4 ไบต์ที่ไม่ได้ลงนาม int ) เป็นประเภทFromและint ที่ไม่ได้ลงนามเป็นประเภทTo :
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
มันเป็นโชคร้ายที่เราต้องประเภทกลางนี้ แต่ที่เป็นข้อ จำกัด ในปัจจุบันของbit_cast
เราไม่มีเครื่องมือที่ดีมากมายในการตรวจจับนามแฝงที่เข้มงวดใน C ++ เครื่องมือที่เรามีจะจับบางกรณีของการละเมิดนามแฝงที่เข้มงวดและบางกรณีของการโหลดและร้านค้าที่ไม่ตรงแนว
gcc โดยใช้แฟล็ก -fstrict-aliasingและ-Wstrict-aliasingสามารถตรวจพบบางกรณีแม้ว่าจะไม่ได้ผลบวก / เชิงลบ ตัวอย่างเช่นกรณีต่อไปนี้จะสร้างคำเตือนเป็น gcc ( ดูแบบสด ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
แม้ว่ามันจะไม่ได้จับกรณีเพิ่มเติมนี้ ( ดูสด ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
แม้ว่าเสียงดังกราวด์อนุญาตให้ใช้ธงเหล่านี้ แต่ดูเหมือนว่าไม่ได้ใช้คำเตือนจริง ๆ
เครื่องมืออีกอย่างที่เรามีให้สำหรับเราคือ ASan ซึ่งสามารถรับโหลดที่ไม่ตรงแนวและเก็บได้ แม้ว่าสิ่งเหล่านี้ไม่ใช่การละเมิดนามแฝงที่เข้มงวดโดยตรง แต่เป็นผลทั่วไปของการละเมิดนามแฝงที่เข้มงวด ตัวอย่างเช่นกรณีต่อไปนี้จะสร้างข้อผิดพลาดรันไทม์เมื่อสร้างด้วย clang โดยใช้-fsanitize = address
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
เครื่องมือสุดท้ายที่ฉันจะแนะนำคือเฉพาะ C ++ และไม่ใช่เครื่องมืออย่างเคร่งครัด แต่เป็นการฝึกเขียนโค้ดไม่อนุญาตให้ใช้การโยนแบบ C ทั้งสอง GCC และเสียงดังกราวจะผลิตการวินิจฉัยสำหรับบรรยากาศแบบ C ใช้-Wold สไตล์หล่อ สิ่งนี้จะบังคับให้การพิมพ์ที่ไม่ได้กำหนดใด ๆ เพื่อใช้ reinterpret_cast โดยทั่วไป reinterpret_cast ควรเป็นค่าสถานะสำหรับการตรวจสอบโค้ดที่ใกล้ชิดยิ่งขึ้น นอกจากนี้ยังง่ายต่อการค้นหาฐานรหัสของคุณสำหรับ reinterpret_cast เพื่อทำการตรวจสอบ
สำหรับ C เรามีเครื่องมือทั้งหมดครอบคลุมอยู่แล้วและเรายังมี tis-interpreter ตัววิเคราะห์แบบคงที่ที่วิเคราะห์โปรแกรมสำหรับชุดย่อยขนาดใหญ่ของภาษา C รับ verion C ของตัวอย่างก่อนหน้านี้ที่ใช้-fstrict-aliasingคิดถึงหนึ่งกรณี ( ดูแบบสด )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter สามารถจับทั้งสามตัวอย่างต่อไปนี้เรียก tis-kernal เป็น tis-interpreter (เอาต์พุตถูกแก้ไขเพื่อความกระชับ):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
ในที่สุดก็มีTySanซึ่งขณะนี้อยู่ในการพัฒนา น้ำยาฆ่าเชื้อนี้เพิ่มข้อมูลการตรวจสอบประเภทในเซ็กเมนต์หน่วยความจำเงาและตรวจสอบการเข้าถึงเพื่อดูว่าพวกเขาละเมิดกฎนามแฝงหรือไม่ เครื่องมือนี้ควรจะสามารถตรวจจับการละเมิดนามแฝงทั้งหมด แต่อาจมีค่าใช้จ่ายในการดำเนินการจำนวนมาก
reinterpret_cast
จะทำอย่างไรหรือcout
อาจหมายถึงอะไร (เป็นเรื่องที่ถูกต้องที่จะพูดถึง C ++ แต่คำถามเดิมเกี่ยวกับ C และ IIUC ตัวอย่างเหล่านี้สามารถเขียนได้อย่างถูกต้องในภาษาซี)
นามแฝงที่เข้มงวดไม่ได้หมายถึงเฉพาะพอยน์เตอร์เท่านั้น แต่มีผลต่อการอ้างอิงด้วยเช่นกันฉันเขียนบทความเกี่ยวกับวิกิพีเดียสำหรับนักพัฒนาบูสเตอร์และได้รับการตอบรับเป็นอย่างดี มันอธิบายได้อย่างสมบูรณ์ว่ามันคืออะไรทำไมมันทำให้คนสับสนมากและจะทำอย่างไรกับมัน เข้มงวดนามแฝงกระดาษสีขาว โดยเฉพาะอย่างยิ่งมันอธิบายว่าทำไมสหภาพแรงงานจึงมีพฤติกรรมเสี่ยงสำหรับ C ++ และทำไมการใช้ memcpy จึงเป็นตัวแก้ไขแบบพกพาเพียงตัวเดียวสำหรับทั้ง C และ C ++ หวังว่านี่จะเป็นประโยชน์
ภาคผนวกของสิ่งที่ Doug T. เขียนไว้แล้วนี่เป็นกรณีทดสอบอย่างง่ายซึ่งอาจเรียกใช้ด้วย gcc:
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 ส่วนใหญ่ที่ฉันพยายาม) เอาท์พุทนี้ "ปัญหานามแฝงที่เข้มงวด" เพราะคอมไพเลอร์ถือว่า "h" ไม่สามารถเป็นที่อยู่เดียวกับ "k" ในฟังก์ชั่น "ตรวจสอบ" เพราะการที่คอมไพเลอร์เพิ่มประสิทธิภาพif (*h == 5)
ออกไปและเรียก printf
สำหรับผู้ที่สนใจที่นี่คือรหัสแอสเซมเบลอร์ x64 ที่ผลิตโดย gcc 4.6.3 ทำงานบน ubuntu 12.04.2 สำหรับ x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
ดังนั้นถ้าเงื่อนไขหายไปจากรหัสแอสเซมเบลอร์อย่างสมบูรณ์
long long*
และint64_t
*) หนึ่งอาจคาดหวังว่าคอมไพเลอร์สติควรตระหนักว่าlong long*
และint64_t*
สามารถเข้าถึงที่เก็บข้อมูลเดียวกันหากพวกเขาเก็บไว้เหมือนกัน แต่การรักษาดังกล่าวไม่เป็นที่นิยมอีกต่อไป
พิมพ์ punningผ่านทาง casts พอยน์เตอร์ (ซึ่งต่างจากการใช้ยูเนี่ยน) เป็นตัวอย่างที่สำคัญของการแยก aliasing ที่เข้มงวด
fpsync()
คำสั่งระหว่างการเขียนเป็น fp และการอ่านเป็น int หรือในทางกลับกัน [ในการใช้งานที่มีจำนวนเต็มแยกและท่อ FPU และแคช คำสั่งดังกล่าวอาจมีราคาแพง แต่ไม่คุ้มค่าเท่ากับการให้คอมไพเลอร์ดำเนินการซิงโครไนซ์ดังกล่าวในทุกการเข้าถึงยูเนี่ยน] หรือการนำไปปฏิบัติสามารถระบุว่าค่าผลลัพธ์จะไม่สามารถใช้งานได้ยกเว้นในกรณีที่ใช้ลำดับเริ่มต้นทั่วไป
ตามเหตุผลของ C89 ผู้เขียนมาตรฐานไม่ต้องการให้คอมไพเลอร์ให้รหัสเหมือน:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
ควรจะต้องโหลดค่าของx
ระหว่างการกำหนดและผลตอบแทนคำสั่งเพื่อให้เป็นเพื่อให้เป็นไปได้ว่าp
จุดอาจจะx
และมอบหมายให้อาจส่งผลให้การปรับเปลี่ยนค่าของ*p
x
ความคิดที่ว่าคอมไพเลอร์ควรมีสิทธิ์ทึกทักว่าจะไม่มีนามแฝงในสถานการณ์อย่างที่กล่าวมาข้างต้นนั้นไม่ขัดแย้งกัน
น่าเสียดายที่ผู้เขียน C89 เขียนกฎของพวกเขาในลักษณะที่ถ้าอ่านตามตัวอักษรจะทำให้ฟังก์ชั่นต่อไปนี้สามารถเรียกใช้พฤติกรรมที่ไม่ได้กำหนดได้
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
เพราะมันใช้ lvalue ประเภทint
ในการเข้าถึงวัตถุของการพิมพ์struct S
และไม่ได้เป็นหนึ่งชนิดที่อาจจะใช้ในการเข้าถึงint
struct S
เพราะมันจะไร้สาระที่จะปฏิบัติต่อการใช้งานที่ไม่ใช่ตัวละครประเภทสมาชิกของ structs และสหภาพแรงงานเป็นพฤติกรรมที่ไม่ได้กำหนดเกือบทุกคนตระหนักว่ามีอย่างน้อยบางสถานการณ์ที่ lvalue ของประเภทหนึ่งอาจใช้ในการเข้าถึงวัตถุประเภทอื่น . น่าเสียดายที่คณะกรรมการมาตรฐาน C ล้มเหลวในการกำหนดว่าสถานการณ์เหล่านั้นคืออะไร
ปัญหาส่วนใหญ่เป็นผลมาจากข้อบกพร่องรายงาน # 028 ซึ่งถามเกี่ยวกับพฤติกรรมของโปรแกรมเช่น:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
รายงานข้อบกพร่อง # 28 ระบุว่าโปรแกรมเรียกใช้พฤติกรรมที่ไม่ได้กำหนดเนื่องจากการกระทำของการเขียนสมาชิกสหภาพประเภท "double" และการอ่านหนึ่งในประเภท "int" ก่อให้เกิดพฤติกรรมการใช้งานที่กำหนด เหตุผลดังกล่าวไร้สาระ แต่เป็นพื้นฐานสำหรับกฎประเภทที่มีประสิทธิภาพซึ่งไม่จำเป็นต้องใช้ภาษาที่ซับซ้อนในขณะที่ไม่ทำอะไรเพื่อแก้ไขปัญหาเดิม
วิธีที่ดีที่สุดในการแก้ไขปัญหาดั้งเดิมอาจเป็นการรักษาเชิงอรรถเกี่ยวกับวัตถุประสงค์ของกฎราวกับว่าเป็นกฎเกณฑ์และทำให้กฎไม่สามารถบังคับใช้ได้ยกเว้นในกรณีที่เกี่ยวข้องกับการเข้าถึงที่ขัดแย้งกันโดยใช้นามแฝง รับบางสิ่งเช่น:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
ไม่มีความขัดแย้งภายในinc_int
เนื่องจากการเข้าถึงที่จัดเก็บข้อมูลที่เข้าถึงได้*p
ทั้งหมดนั้นกระทำโดยใช้ชนิดที่int
มีค่าน้อยและไม่มีข้อขัดแย้งtest
เนื่องจากp
จะเห็นได้ชัดจาก a struct S
และในครั้งต่อไปs
จะมีการใช้งานทั้งหมดการเข้าถึงที่เก็บข้อมูลนั้น ๆ ผ่านp
จะได้เกิดขึ้นแล้ว
หากรหัสถูกเปลี่ยนเล็กน้อย ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
ที่นี่มีความขัดแย้งระหว่าง aliasing p
และการเข้าถึงs.x
ในบรรทัดที่ทำเครื่องหมายไว้เพราะที่จุดในการดำเนินการที่อ้างอิงอื่นที่มีอยู่ที่จะใช้ในการเข้าถึงที่จัดเก็บข้อมูลเดียวกัน
หากรายงานข้อบกพร่อง 028 กล่าวว่าตัวอย่างดั้งเดิมที่เรียกใช้ UB เนื่องจากการทับซ้อนระหว่างการสร้างและการใช้งานตัวชี้สองตัวซึ่งจะทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้นโดยไม่ต้องเพิ่ม "ประเภทที่มีประสิทธิภาพ" หรือความซับซ้อนอื่น ๆ
หลังจากอ่านคำตอบมากมายฉันรู้สึกว่าต้องเพิ่มบางสิ่ง:
นามแฝงที่เข้มงวด (ซึ่งฉันจะอธิบายเล็กน้อย) มีความสำคัญเนื่องจาก :
การเข้าถึงหน่วยความจำอาจมีราคาแพง (ประสิทธิภาพฉลาด) ซึ่งเป็นเหตุผลที่ข้อมูลถูกจัดการในการลงทะเบียน CPUก่อนที่จะถูกเขียนกลับไปยังหน่วยความจำกายภาพ
หากข้อมูลในการลงทะเบียน CPU ที่แตกต่างกันสองรายการจะถูกเขียนไปยังพื้นที่หน่วยความจำเดียวกันเราไม่สามารถทำนายได้ว่าข้อมูลใดจะ "อยู่รอด"เมื่อเราใช้รหัสใน C
ในแอสเซมบลีที่เราโค้ดการโหลดและการยกเลิกการลงทะเบียน CPU ด้วยตนเองเราจะทราบว่าข้อมูลใดยังคงไม่เปลี่ยนแปลง แต่ C (ขอบคุณ) สรุปรายละเอียดนี้ออกไป
ตั้งแต่สองตัวชี้สามารถชี้ไปที่สถานที่เดียวกันในหน่วยความจำนี้อาจส่งผลในรหัสที่ซับซ้อนที่จับชนที่เป็นไปได้
รหัสพิเศษนี้ทำงานช้าและทำให้ประสิทธิภาพลดลงเนื่องจากจะทำการอ่าน / เขียนหน่วยความจำเพิ่มเติมซึ่งช้าลงและไม่จำเป็น
กฎ aliasing เข้มงวดช่วยให้เราสามารถหลีกเลี่ยงเครื่องรหัสซ้ำซ้อนในกรณีที่มันควรจะปลอดภัยที่จะคิดว่าทั้งสองตัวชี้ไม่ได้ชี้ไปบล็อกหน่วยความจำเดียวกัน (ดูยังrestrict
คำหลัก)
นามแฝงที่เข้มงวดระบุว่าการพอยน์เตอร์ที่ชี้ไปยังประเภทที่แตกต่างกันนั้นปลอดภัยที่จะระบุตำแหน่งต่าง ๆ ในหน่วยความจำ
หากคอมไพเลอร์สังเกตเห็นว่าพอยน์เตอร์สองตัวชี้ไปที่ประเภทที่แตกต่างกัน (เช่นint *
a และ a float *
) จะถือว่าที่อยู่หน่วยความจำแตกต่างกันและจะไม่ป้องกันการชนกันของหน่วยความจำทำให้รหัสเครื่องเร็วขึ้น
ตัวอย่างเช่น :
ให้ถือว่าฟังก์ชันต่อไปนี้:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
เพื่อจัดการกรณีที่a == b
(ตัวชี้ทั้งสองชี้ไปที่หน่วยความจำเดียวกัน) เราจำเป็นต้องสั่งและทดสอบวิธีที่เราโหลดข้อมูลจากหน่วยความจำไปยังการลงทะเบียน CPU ดังนั้นรหัสอาจสิ้นสุดลงเช่นนี้
โหลดa
และb
จากหน่วยความจำ
เพิ่มไปa
b
บันทึก b
และโหลด a
(บันทึกจาก CPU register ไปยังหน่วยความจำและโหลดจากหน่วยความจำไปยัง CPU register)
เพิ่มไปb
a
บันทึกa
(จากการลงทะเบียน CPU) ไปยังหน่วยความจำ
ขั้นตอนที่ 3 ช้ามากเพราะต้องการเข้าถึงหน่วยความจำกายภาพ อย่างไรก็ตามจำเป็นต้องป้องกันอินสแตนซ์ที่a
และb
ชี้ไปยังที่อยู่หน่วยความจำเดียวกัน
นามแฝงที่เข้มงวดจะช่วยให้เราสามารถป้องกันสิ่งนี้ได้โดยบอกคอมไพเลอร์ว่าที่อยู่หน่วยความจำเหล่านี้แตกต่างกันอย่างชัดเจน (ซึ่งในกรณีนี้จะช่วยให้การปรับให้เหมาะสมยิ่งขึ้นซึ่งไม่สามารถทำได้
สิ่งนี้สามารถบอกกับคอมไพเลอร์ได้สองวิธีโดยใช้ประเภทที่แตกต่างกันในการชี้ไปที่ เช่น:
void merge_two_numbers(int *a, long *b) {...}
การใช้restrict
คำสำคัญ เช่น:
void merge_two_ints(int * restrict a, int * restrict b) {...}
ทีนี้จากการปฏิบัติตามกฎ Strict Aliasing ขั้นตอนที่ 3 สามารถหลีกเลี่ยงได้และโค้ดจะทำงานได้เร็วขึ้นอย่างมีนัยสำคัญ
อันที่จริงแล้วโดยการเพิ่มrestrict
คำหลักฟังก์ชันทั้งหมดสามารถปรับให้เหมาะกับ:
โหลดa
และb
จากหน่วยความจำ
เพิ่มไปa
b
บันทึกผลลัพธ์ทั้งไปa
และb
กลับ
การเพิ่มประสิทธิภาพนี้ไม่เคยทำมาก่อนเนื่องจากการชนกันที่เป็นไปได้ (ที่ไหนa
และb
จะเพิ่มเป็นสามเท่าแทนที่จะเป็นสองเท่า)
b
(ไม่โหลดมัน) a
และโหลด ฉันหวังว่ามันชัดเจนขึ้นตอนนี้
restrict
แต่ฉันคิดว่าส่วนใหญ่จะมีประสิทธิภาพมากกว่าและข้อ จำกัด บางประการเกี่ยวกับการregister
อนุญาตให้กรอกในบางกรณีที่restrict
ไม่สามารถช่วยได้ ฉันไม่แน่ใจว่ามันเป็น "สำคัญ" ต่อการปฏิบัติตามมาตรฐานอย่างเต็มที่ในการอธิบายทุกกรณีที่โปรแกรมเมอร์ควรคาดหวังว่าคอมไพเลอร์จะรับรู้หลักฐานของนามแฝงแทนที่จะอธิบายเพียงสถานที่ที่คอมไพเลอร์ต้องเข้าใจนามแฝงแม้ว่าจะไม่มีหลักฐานใดๆ
restrict
คำหลักยังลดความเร็วของการดำเนินการให้น้อยที่สุด แต่ยังช่วยลดจำนวนการดำเนินการซึ่งอาจมีความหมาย ... ฉันหมายถึงหลังจากนั้นการดำเนินการที่เร็วที่สุดก็ไม่สามารถใช้งานได้เลย :)
นามแฝงที่เข้มงวดไม่อนุญาตให้ตัวชี้ชนิดต่าง ๆ ไปยังข้อมูลเดียวกัน
บทความนี้จะช่วยให้คุณเข้าใจปัญหาโดยละเอียด
int
และโครงสร้างที่มีint
)
ในทางเทคนิคใน C ++ กฎนามแฝงที่เข้มงวดนั้นอาจไม่สามารถใช้ได้
หมายเหตุคำจำกัดความของการส่งข้อมูลทางอ้อม ( * โอเปอเรเตอร์ ):
ตัวดำเนินการ unary * ดำเนินการทางอ้อม: นิพจน์ที่ใช้จะเป็นตัวชี้ไปยังชนิดของวัตถุหรือตัวชี้ไปยังประเภทฟังก์ชันและผลลัพธ์คือค่า lvalue ที่อ้างถึงวัตถุหรือฟังก์ชันที่จุดแสดงออกนั้น
นอกจากนี้จากคำจำกัดความของ glvalue
glvalue คือนิพจน์ที่การประเมินผลกำหนดตัวตนของวัตถุ (... snip)
ดังนั้นในการติดตามโปรแกรมที่กำหนดไว้อย่างดี glvalue หมายถึงวัตถุ ดังนั้นกฎนามแฝงที่เข้มงวดจึงไม่มีผลบังคับใช้ นี่อาจไม่ใช่สิ่งที่นักออกแบบต้องการ
int foo;
สิ่งที่เข้าถึงได้โดยการแสดงออก lvalue *(char*)&foo
? นั่นเป็นวัตถุประเภทchar
ใช่หรือไม่ วัตถุนั้นเกิดขึ้นพร้อมกันfoo
หรือไม่? จะเขียนเพื่อfoo
เปลี่ยนค่าที่เก็บไว้ของวัตถุประเภทดังกล่าวข้างต้นchar
หรือไม่ ถ้าเป็นเช่นนั้นมีกฎใดบ้างที่จะอนุญาตให้ค่าที่เก็บไว้ของวัตถุประเภทนั้นchar
สามารถเข้าถึงได้โดยใช้ lvalue ชนิดint
?
int i;
สร้างสี่วัตถุประเภทตัวละครแต่ละตัวin addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) และ i` i
และ ในที่สุดก็ไม่มีอะไรใน Standard ที่อนุญาตให้แม้แต่volatile
ตัวชี้ที่มีคุณสมบัติในการเข้าถึงการลงทะเบียนฮาร์ดแวร์ที่ไม่ตรงกับคำจำกัดความของ "object"
c
c++faq