กฎนามแฝงที่เข้มงวดคืออะไร


804

เมื่อถามถึงพฤติกรรมที่ไม่ได้กำหนดร่วมกันใน Cบางครั้งผู้คนอ้างถึงกฎนามแฝงที่เข้มงวด
พวกเขากำลังพูดเกี่ยวกับอะไร?


12
@Ben Voigt: กฎนามแฝงนั้นแตกต่างกันสำหรับ c ++ และ c ทำไมคำถามนี้ติดแท็กด้วยและc c++faq
MikeMB

6
@MikeMB: หากคุณตรวจสอบประวัติคุณจะเห็นว่าฉันเก็บแท็กไว้เหมือนเดิมแม้ว่าผู้เชี่ยวชาญจะพยายามเปลี่ยนคำถามจากคำตอบที่มีอยู่ นอกจากนี้การพึ่งพาภาษาและการพึ่งพาเวอร์ชันเป็นส่วนสำคัญของคำตอบของ "กฎนามแฝงที่เข้มงวดคืออะไร" และการรู้ถึงความแตกต่างนั้นมีความสำคัญต่อทีมการโยกย้ายรหัสระหว่าง C และ C ++ หรือการเขียนแมโครเพื่อใช้ในทั้งสองอย่าง
Ben Voigt

6
@Ben Voigt: จริง ๆ แล้ว - เท่าที่ฉันสามารถบอกได้ - คำตอบส่วนใหญ่เกี่ยวข้องกับ c เท่านั้นและไม่ใช่ c ++ และถ้อยคำของคำถามบ่งบอกถึงการมุ่งเน้นไปที่ C-rules (หรือ OP ไม่ทราบว่ามีความแตกต่าง ) ส่วนใหญ่แล้วกฎและแนวคิดทั่วไปเหมือนกัน แต่โดยเฉพาะอย่างยิ่งเมื่อสหภาพมีความกังวลคำตอบจะไม่มีผลกับ c ++ ฉันกังวลเล็กน้อยว่าโปรแกรมเมอร์ c ++ บางคนจะมองหากฎนามแฝงที่เข้มงวดและจะสมมติว่าทุกอย่างที่ระบุไว้ที่นี่ใช้กับ c ++ ด้วย
MikeMB

ในทางกลับกันฉันยอมรับว่ามันเป็นปัญหาในการเปลี่ยนคำถามหลังจากที่มีการโพสต์คำตอบที่ดีจำนวนมากและปัญหานั้นเป็นเรื่องรองลงมา
MikeMB

1
@MikeMB: ฉันคิดว่าคุณจะเห็นว่าการโฟกัส C กับคำตอบที่ยอมรับทำให้ไม่ถูกต้องสำหรับ C ++ ถูกแก้ไขโดยบุคคลที่สาม ส่วนนั้นควรได้รับการแก้ไขอีกครั้ง
Ben Voigt

คำตอบ:


562

สถานการณ์ทั่วไปที่คุณพบปัญหานามแฝงที่เข้มงวดคือเมื่อวางทับโครงสร้าง (เช่นอุปกรณ์ / เครือข่าย msg) ลงบนบัฟเฟอร์ของขนาดคำของระบบของคุณ (เช่นตัวชี้ไปยังuint32_ts หรือuint16_ts) เมื่อคุณวางทับโครงสร้างลงบนบัฟเฟอร์หรือบัฟเฟอร์ลงบนโครงสร้างผ่านการชี้ตัวชี้คุณสามารถละเมิดกฎนามแฝงที่เข้มงวดได้อย่างง่ายดาย

ดังนั้นในการตั้งค่าแบบนี้ถ้าฉันต้องการส่งข้อความถึงสิ่งที่ฉันจะต้องมีพอยน์เตอร์ที่เข้ากันไม่ได้สองตัวชี้ไปที่หน่วยความจำอันเดียวกัน ฉันอาจไร้เดียงสารหัสเช่นนี้ (ในระบบด้วย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 ได้:

  • ประเภทที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ
  • รุ่นที่ผ่านการรับรองประเภทที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ
  • ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามที่สอดคล้องกับประเภทที่มีประสิทธิภาพของวัตถุ
  • ประเภทที่เป็นประเภทที่ลงนามหรือไม่ได้ลงนามซึ่งสอดคล้องกับรุ่นที่ผ่านการรับรองของประเภทที่มีประสิทธิภาพของวัตถุ
  • ประเภทรวมหรือสหภาพที่มีหนึ่งในประเภทดังกล่าวในหมู่สมาชิก (รวมถึง recursively เป็นสมาชิกของกลุ่มย่อยหรือสหภาพที่มีอยู่) หรือ
  • ประเภทตัวละคร

16
ฉันจะมาหลังจากการต่อสู้ดูเหมือนว่า .. อาจunsigned char*จะใช้ไกลมากchar*แทนไหม? ฉันมักจะใช้unsigned charแทนที่จะcharเป็นประเภทพื้นฐานbyteเพราะไบต์ของฉันไม่ได้ลงนามและฉันไม่ต้องการความแปลกประหลาดของพฤติกรรมที่ลงชื่อ (โดยเฉพาะอย่างยิ่ง WRT เพื่อล้น)
Matthieu M.

30
@ Matthieu: Signedness ไม่ต่างกับกฎนามแฝงดังนั้นการใช้unsigned char *ก็โอเค
โทมัส Eding

22
พฤติกรรมที่ไม่ได้กำหนดเพื่ออ่านจากสมาชิกสหภาพแตกต่างจากการเขียนครั้งสุดท้ายใช่หรือไม่
R. Martinho Fernandes

23
ไร้สาระคำตอบนี้เป็นอย่างสมบูรณ์ไปข้างหลัง ตัวอย่างที่แสดงว่าผิดกฎหมายนั้นถูกกฎหมายจริงและตัวอย่างที่แสดงว่าถูกกฎหมายนั้นผิดกฎหมาย
R. Martinho Fernandes

7
uint32_t* buff = malloc(sizeof(Msg));การunsigned int asBuffer[sizeof(Msg)];ประกาศสหภาพบัฟเฟอร์ของคุณและภายหลังจะมีขนาดแตกต่างกันและไม่ถูกต้อง การmallocเรียกใช้การจัดตำแหน่งแบบ 4 ไบต์ภายใต้ประทุน (ไม่ต้องทำ) และการรวมจะยิ่งใหญ่กว่าที่จำเป็นต้องเป็น 4 เท่า ... ฉันเข้าใจว่าเป็นความชัดเจน แต่ไม่มีข้อผิดพลาดสำหรับฉัน น้อยลง ...
nonsensickle

233

คำอธิบายที่ดีที่สุดที่ฉันได้พบคือโดยไมค์แอคตันเข้าใจแฝงเข้มงวด มันมุ่งเน้นไปที่การพัฒนา PS3 เพียงเล็กน้อย แต่โดยทั่วไปเป็นเพียง GCC

จากบทความ:

"Strict aliasing เป็นข้อสันนิษฐานที่สร้างขึ้นโดยคอมไพเลอร์ C (หรือ C ++) ที่การยกเลิกการชี้พอยน์เตอร์ไปยังวัตถุประเภทต่าง ๆ จะไม่อ้างถึงตำแหน่งหน่วยความจำเดียวกัน (เช่นนามแฝงซึ่งกันและกัน)"

ดังนั้นโดยทั่วไปถ้าคุณมีการint*ชี้ไปยังหน่วยความจำที่มีintและจากนั้นคุณชี้float*ไปที่หน่วยความจำนั้นและใช้เป็นfloatคุณทำลายกฎ หากรหัสของคุณไม่เคารพสิ่งนี้ตัวเพิ่มประสิทธิภาพของคอมไพเลอร์จะทำลายรหัสของคุณมากที่สุด

ข้อยกเว้นของกฎคือ a char*ซึ่งได้รับอนุญาตให้ชี้ไปที่ประเภทใดก็ได้


6
ดังนั้นวิธีบัญญัติของการใช้หน่วยความจำเดียวกันกับตัวแปรที่แตกต่างกัน 2 ประเภทคืออะไร? หรือทุกคนแค่คัดลอก?
jiggunjer

4
หน้าของ Mike Acton มีข้อบกพร่อง ส่วนหนึ่งของ "การคัดเลือกนักแสดง (2)" อย่างน้อยที่สุดก็เป็นเรื่องที่ผิด รหัสที่เขาอ้างว่าถูกกฎหมายไม่ใช่
davmac

11
@davmac: ผู้เขียน C89 ไม่เคยตั้งใจว่าควรบังคับให้โปรแกรมเมอร์ข้ามผ่านห่วง ฉันพบความคิดที่แปลกประหลาดอย่างทั่วถึงว่ากฎที่มีอยู่เพื่อวัตถุประสงค์ในการเพิ่มประสิทธิภาพเพียงอย่างเดียวควรถูกตีความในลักษณะที่ต้องการให้โปรแกรมเมอร์เขียนรหัสที่ทำสำเนาข้อมูลซ้ำซ้อนด้วยความหวังว่าเครื่องมือเพิ่มประสิทธิภาพจะลบรหัสซ้ำซ้อน
supercat

1
@curtguy: "ไม่มีสหภาพ" หรือไม่? ประการแรกวัตถุประสงค์ดั้งเดิม / หลักของสหภาพไม่ได้เกี่ยวข้องกับนามแฝง แต่อย่างใด ประการที่สองสเป็คภาษาสมัยใหม่อนุญาตให้ใช้สหภาพอย่างชัดเจนสำหรับนามแฝง คอมไพเลอร์จะต้องแจ้งให้ทราบว่ามีการใช้สหภาพและจัดการกับสถานการณ์เป็นวิธีพิเศษ
AnT

5
@currguy: เท็จ ประการแรกแนวคิดทางความคิดดั้งเดิมที่อยู่เบื้องหลังสหภาพคือเมื่อใดก็ตามที่มีวัตถุสมาชิกเพียงชิ้นเดียวที่ "ทำงาน" ในวัตถุสหภาพที่กำหนดในขณะที่คนอื่น ๆ ก็ไม่มีอยู่ ดังนั้นจึงไม่มี "วัตถุต่าง ๆ ที่อยู่เดียวกัน" ตามที่คุณเชื่อ ประการที่สองการละเมิดนามแฝงที่ทุกคนพูดถึงนั้นเกี่ยวกับการเข้าถึงวัตถุหนึ่งเป็นวัตถุที่แตกต่างไม่ใช่เพียงแค่มีสองวัตถุที่มีที่อยู่เดียวกัน ตราบใดที่ไม่มีการเข้าถึงแบบอักษรตัวเล็กก็ไม่มีปัญหา นั่นคือความคิดดั้งเดิม ต่อมาอนุญาตให้พิมพ์การสะกดคำผ่านสหภาพ
ANT

133

นี่เป็นกฎนามแฝงที่เข้มงวดซึ่งพบในส่วน 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)ความตั้งใจของรายการนี้คือการระบุสถานการณ์เหล่านั้นที่วัตถุอาจหรือไม่ได้รับนามแฝง


7
เบ็นเนื่องจากผู้คนมักจะมาที่นี่ฉันได้อนุญาตให้ฉันเพิ่มการอ้างอิงถึงมาตรฐาน C ด้วยเพื่อความสมบูรณ์
Kos

1
ดูที่ C89 Rationale cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdfส่วน 3.3 ที่พูดถึง
phorgan1

2
ถ้ามี lvalue ของชนิดโครงสร้างใช้ที่อยู่ของสมาชิกและส่งผ่านไปยังฟังก์ชันที่ใช้เป็นตัวชี้ไปยังชนิดสมาชิกจะถือว่าเป็นการเข้าถึงวัตถุประเภทสมาชิก (ถูกกฎหมาย) หรือวัตถุประเภทโครงสร้าง (ต้องห้าม)? จำนวนมากของรหัสที่ถือว่ามันเป็นกฎหมายที่จะเข้าถึงโครงสร้างในแฟชั่นดังกล่าวและผมคิดว่าคนจำนวนมากจะบ่นที่กฎซึ่งเข้าใจว่าเป็นห้ามการกระทำดังกล่าว แต่ก็ไม่มีความชัดเจนว่ากฎระเบียบที่แน่นอน นอกจากนี้สหภาพและโครงสร้างจะได้รับการปฏิบัติเหมือนกัน แต่กฎที่สมเหตุสมผลสำหรับแต่ละข้อควรแตกต่างกัน
supercat

2
@supercat: วิธีการใช้กฎสำหรับโครงสร้างคำพูดการเข้าถึงที่แท้จริงมักจะเป็นประเภทดั้งเดิม จากนั้นการเข้าถึงผ่านการอ้างอิงถึงประเภทดั้งเดิมนั้นถูกกฎหมายเพราะประเภทตรงกันและการเข้าถึงผ่านการอ้างอิงถึงประเภทโครงสร้างที่มีอยู่นั้นถูกกฎหมายเพราะได้รับอนุญาตเป็นพิเศษ
Ben Voigt

2
@BenVoigt: ฉันไม่คิดว่าการเริ่มต้นลำดับทั่วไปทำงานได้เว้นแต่ว่าการเข้าถึงจะทำผ่านทางสหภาพ ดูgoo.gl/HGOyoKเพื่อดูว่า gcc กำลังทำอะไร หากเข้าถึง lvalue ประเภทสหภาพผ่าน lvalue ประเภทสมาชิกที่ใช้งาน (ไม่ได้ใช้ประกอบการสหภาพสมาชิกเข้าถึง) ถูกต้องตามกฎหมายแล้วwow(&u->s1,&u->s2)จะต้องถูกต้องตามกฎหมายแม้ในขณะที่ตัวชี้จะใช้ในการปรับเปลี่ยนuและที่จะปฏิเสธการเพิ่มประสิทธิภาพมากที่สุดว่า กฎนามแฝงถูกออกแบบมาเพื่ออำนวยความสะดวก
supercat

81

บันทึก

นี่เป็นข้อความที่ตัดตอนมาจาก"กฎนามแฝงที่เข้มงวดของฉันคืออะไรและทำไมเราจึงสนใจ" เขียน.

นามแฝงที่เข้มงวดคืออะไร

ในการสร้างสมนาม 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 ให้เหมาะสม

ตอนนี้เพื่อ Rule-Book

มาตรฐานบอกอะไรเราว่าได้รับอนุญาตและไม่ได้รับอนุญาตให้ทำ? ภาษามาตรฐานไม่ตรงไปตรงมาดังนั้นสำหรับแต่ละรายการฉันจะพยายามให้ตัวอย่างรหัสที่แสดงให้เห็นถึงความหมาย

มาตรฐาน C11 พูดว่าอะไร?

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 พูด

มาตรฐานฉบับร่าง 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ซึ่งบอกว่าเป็นชนิดตัวอักษร

Type Punning คืออะไร

เราได้มาถึงจุดนี้และเราอาจสงสัยว่าทำไมเราต้องการนามแฝง? คำตอบคือพิมพ์คำพิพากษาบ่อยครั้งวิธีที่ใช้ละเมิดกฎนามแฝงที่เข้มงวด

บางครั้งเราต้องการหลีกเลี่ยงระบบชนิดและตีความวัตถุเป็นชนิดอื่น สิ่งนี้เรียกว่าประเภท 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 เป็นการละเมิด

เราจะพิมพ์ Pun อย่างไรให้ถูกต้อง

วิธีการมาตรฐานสำหรับประเภทเล่นสำนวนทั้ง 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

ใน 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ซึ่งขณะนี้อยู่ในการพัฒนา น้ำยาฆ่าเชื้อนี้เพิ่มข้อมูลการตรวจสอบประเภทในเซ็กเมนต์หน่วยความจำเงาและตรวจสอบการเข้าถึงเพื่อดูว่าพวกเขาละเมิดกฎนามแฝงหรือไม่ เครื่องมือนี้ควรจะสามารถตรวจจับการละเมิดนามแฝงทั้งหมด แต่อาจมีค่าใช้จ่ายในการดำเนินการจำนวนมาก


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Bhargav Rao

3
ถ้าฉันทำได้, +10, เขียนได้ดีและอธิบายได้เช่นกันจากทั้งสองฝ่ายนักเขียนคอมไพเลอร์และโปรแกรมเมอร์ ... การวิจารณ์เพียงอย่างเดียว: มันจะดีถ้ามีตัวอย่างที่เคาน์เตอร์ด้านบนเพื่อดูว่าอะไรเป็นสิ่งต้องห้ามตามมาตรฐาน ชนิด :-)
Gabriel

2
คำตอบที่ดีมาก ฉันเสียใจที่ตัวอย่างแรกเริ่มมีให้ใน C ++ ซึ่งทำให้ยากต่อการติดตามสำหรับคนอย่างฉันที่รู้หรือสนใจเกี่ยวกับ C เท่านั้นและไม่รู้ว่าreinterpret_castจะทำอย่างไรหรือcoutอาจหมายถึงอะไร (เป็นเรื่องที่ถูกต้องที่จะพูดถึง C ++ แต่คำถามเดิมเกี่ยวกับ C และ IIUC ตัวอย่างเหล่านี้สามารถเขียนได้อย่างถูกต้องในภาษาซี)
Gro-Tsen

เกี่ยวกับการ puning ประเภท: ดังนั้นถ้าฉันเขียน array ของ type X ลงในไฟล์ให้อ่านจากไฟล์นั้นใน array ที่ชี้ด้วยโมฆะ * จากนั้นฉันโยนพอยเตอร์นั้นไปยังชนิดข้อมูลจริงเพื่อใช้งาน - นั่นคือ พฤติกรรมที่ไม่ได้กำหนด?
Michael IV

44

นามแฝงที่เข้มงวดไม่ได้หมายถึงเฉพาะพอยน์เตอร์เท่านั้น แต่มีผลต่อการอ้างอิงด้วยเช่นกันฉันเขียนบทความเกี่ยวกับวิกิพีเดียสำหรับนักพัฒนาบูสเตอร์และได้รับการตอบรับเป็นอย่างดี มันอธิบายได้อย่างสมบูรณ์ว่ามันคืออะไรทำไมมันทำให้คนสับสนมากและจะทำอย่างไรกับมัน เข้มงวดนามแฝงกระดาษสีขาว โดยเฉพาะอย่างยิ่งมันอธิบายว่าทำไมสหภาพแรงงานจึงมีพฤติกรรมเสี่ยงสำหรับ C ++ และทำไมการใช้ memcpy จึงเป็นตัวแก้ไขแบบพกพาเพียงตัวเดียวสำหรับทั้ง C และ C ++ หวังว่านี่จะเป็นประโยชน์


3
" aliasing เข้มงวดไม่ได้หมายเพียงเพื่อที่จะชี้จะมีผลต่อการอ้างอิงเช่นกัน " จริงๆแล้วมันหมายถึงlvalues " ใช้ memcpy เป็นอุปกรณ์พกพาตัวเดียว " Hear!
1111

5
กระดาษที่ดี สิ่งที่ฉันใช้: (1) นามแฝงนี้ - 'ปัญหา' เป็นปฏิกิริยาที่มีต่อการเขียนโปรแกรมที่ไม่ดี - พยายามปกป้องโปรแกรมเมอร์ที่ไม่ดีจากพฤติกรรมที่ไม่ดีของเขา / เธอ หากโปรแกรมเมอร์มีนิสัยที่ดีการใช้นามแฝงนี้เป็นเพียงความรำคาญและสามารถปิดการตรวจสอบได้อย่างปลอดภัย (2) การเพิ่มประสิทธิภาพคอมไพเลอร์ด้านควรทำในกรณีที่รู้จักกันดีและควรสงสัยเมื่อปฏิบัติตามอย่างเคร่งครัดซอร์สโค้ด; การบังคับให้โปรแกรมเมอร์เขียนโค้ดเพื่อรองรับไอดีของคอมไพเลอร์ก็คือใส่ผิด ยิ่งแย่ไปกว่านั้นเพื่อทำให้มันเป็นส่วนหนึ่งของมาตรฐาน
slashmais

4
@slashmais (1) " เป็นปฏิกิริยาที่ไม่ดีต่อการเขียนโปรแกรมที่ไม่ดี " ไร้สาระ มันเป็นการปฏิเสธนิสัยที่ไม่ดี คุณทำมัน? คุณจ่ายราคา: ไม่มีการรับประกันสำหรับคุณ! (2) กรณีที่รู้จักกันดี? อันไหน? กฎนามแฝงที่เข้มงวดควร "รู้จักกันดี"!
curiousguy

5
@crownguy: หลังจากล้างความสับสนเล็กน้อยแล้วเห็นได้ชัดว่าภาษา C กับกฎนามแฝงทำให้โปรแกรมไม่สามารถใช้พูลหน่วยความจำแบบไม่เชื่อเรื่องพระเจ้าได้ โปรแกรมบางชนิดสามารถผ่านได้ด้วย malloc / free แต่โปรแกรมอื่น ๆ ต้องการตรรกะการจัดการหน่วยความจำที่เหมาะกับงานในมือมากขึ้น ฉันสงสัยว่าทำไมเหตุผล C89 จึงใช้ตัวอย่างที่เลวร้ายของเหตุผลสำหรับกฎนามแฝงเนื่องจากตัวอย่างของพวกเขาทำให้ดูเหมือนว่ากฎจะไม่ก่อให้เกิดปัญหาใหญ่ในการปฏิบัติงานที่สมเหตุสมผล
supercat

5
@currguy คอมไพเลอร์สวีทส่วนใหญ่มีอยู่รวมถึง -fstrict-aliasing เป็นค่าเริ่มต้นใน -O3 และสัญญาที่ซ่อนอยู่นี้บังคับให้ผู้ใช้ที่ไม่เคยได้ยิน TBAA และเขียนโค้ดเช่นเดียวกับที่โปรแกรมเมอร์ระบบอาจทำ ฉันไม่ได้ตั้งใจจะให้เสียงกับโปรแกรมเมอร์ระบบ แต่การเพิ่มประสิทธิภาพแบบนี้ควรอยู่นอกการเลือกเริ่มต้นของ -O3 และควรเป็นการเพิ่มประสิทธิภาพการเลือกใช้สำหรับผู้ที่รู้ว่า TBAA คืออะไร มันไม่สนุกเลยที่ได้ดูคอมไพเลอร์ 'bug' ที่กลายเป็นรหัสผู้ใช้ที่ละเมิด TBAA โดยเฉพาะการติดตามการละเมิดระดับแหล่งที่มาในรหัสผู้ใช้
kchoi

34

ภาคผนวกของสิ่งที่ 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

ดังนั้นถ้าเงื่อนไขหายไปจากรหัสแอสเซมเบลอร์อย่างสมบูรณ์


ถ้าคุณเพิ่ม short j * ที่สองเพื่อตรวจสอบ () และใช้มัน (* j = 7) ดังนั้นการปรับให้เหมาะสมจะหายไปเนื่องจาก ggc ไม่ได้ถ้า h และ j ไม่ใช่ค่าจริงที่ชี้ไปที่ค่าเดียวกัน ใช่การเพิ่มประสิทธิภาพเป็นสมาร์ทจริงๆ
philippe lhardy

2
หากต้องการทำให้สิ่งต่าง ๆ สนุกสนานยิ่งขึ้นให้ใช้ตัวชี้ไปยังประเภทที่ไม่สามารถใช้งานร่วมกันได้ แต่มีขนาดและการเป็นตัวแทนเดียวกัน (ในบางระบบที่เป็นจริงเช่นlong long*และint64_t*) หนึ่งอาจคาดหวังว่าคอมไพเลอร์สติควรตระหนักว่าlong long*และint64_t*สามารถเข้าถึงที่เก็บข้อมูลเดียวกันหากพวกเขาเก็บไว้เหมือนกัน แต่การรักษาดังกล่าวไม่เป็นที่นิยมอีกต่อไป
supercat

Grr ... x64 เป็นแบบแผนของ Microsoft ใช้ amd64 หรือ x86_64 แทน
SS Anne

Grr ... x64 เป็นแบบแผนของ Microsoft ใช้ amd64 หรือ x86_64 แทน
SS Anne

17

พิมพ์ punningผ่านทาง casts พอยน์เตอร์ (ซึ่งต่างจากการใช้ยูเนี่ยน) เป็นตัวอย่างที่สำคัญของการแยก aliasing ที่เข้มงวด


1
ดูคำตอบของฉันที่นี่สำหรับคำพูดที่เกี่ยวข้องโดยเฉพาะอย่างยิ่งเชิงอรรถแต่การพิมพ์การสะกดคำผ่านสหภาพได้รับอนุญาตใน C เสมอแม้ว่าคำแรกจะไม่ดีนัก คุณต้องการที่จะชี้แจงคำตอบของคุณ
Shafik Yaghmour

@ShafikYaghmour: C89 อนุญาตให้ผู้ปฏิบัติงานเลือกกรณีที่พวกเขาต้องการหรือไม่จดจำการรู้จำเจชนิดที่มีประโยชน์ผ่านทางสหภาพ ยกตัวอย่างเช่นการใช้งานอาจระบุว่าสำหรับการเขียนไปยังหนึ่งประเภทตามด้วยการอ่านของอีกประเภทหนึ่งที่จะรับรู้เป็นประเภท punning ถ้าโปรแกรมเมอร์ทำอย่างใดอย่างหนึ่งต่อไปนี้ระหว่างการเขียนและการอ่าน : (1) ประเมินค่า lvalue ประเภทยูเนี่ยน [รับที่อยู่ของสมาชิกจะมีคุณสมบัติหากทำที่จุดที่ถูกต้องในลำดับ]; (2) แปลงตัวชี้เป็นประเภทหนึ่งเป็นตัวชี้ชนิดหนึ่งและเข้าถึงผ่าน PTR นั้น
supercat

@ShafikYaghmour: การดำเนินการยังสามารถระบุเช่นประเภทที่ punning ระหว่างจำนวนเต็มและค่าทศนิยมจะทำงานได้อย่างน่าเชื่อถือหากรหัสดำเนินการfpsync()คำสั่งระหว่างการเขียนเป็น fp และการอ่านเป็น int หรือในทางกลับกัน [ในการใช้งานที่มีจำนวนเต็มแยกและท่อ FPU และแคช คำสั่งดังกล่าวอาจมีราคาแพง แต่ไม่คุ้มค่าเท่ากับการให้คอมไพเลอร์ดำเนินการซิงโครไนซ์ดังกล่าวในทุกการเข้าถึงยูเนี่ยน] หรือการนำไปปฏิบัติสามารถระบุว่าค่าผลลัพธ์จะไม่สามารถใช้งานได้ยกเว้นในกรณีที่ใช้ลำดับเริ่มต้นทั่วไป
supercat

@ShafikYaghmour: ภายใต้ C89 ใช้งานสามารถห้ามรูปแบบมากที่สุดของประเภทเล่นสำนวนรวมทั้งผ่านทางสหภาพแรงงาน แต่ความเท่าเทียมกันระหว่างตัวชี้ไปยังสหภาพและตัวชี้ไปยังสมาชิกของพวกเขาส่อให้เห็นว่าประเภทเล่นสำนวนได้รับอนุญาตในการใช้งานที่ไม่ชัดห้าม
supercat

17

ตามเหตุผลของ 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 เนื่องจากการทับซ้อนระหว่างการสร้างและการใช้งานตัวชี้สองตัวซึ่งจะทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้นโดยไม่ต้องเพิ่ม "ประเภทที่มีประสิทธิภาพ" หรือความซับซ้อนอื่น ๆ


เอาล่ะมันน่าสนใจที่จะอ่านข้อเสนอแปลก ๆ ที่มากหรือน้อย "สิ่งที่คณะกรรมการมาตรฐานสามารถทำได้" ที่บรรลุเป้าหมายโดยไม่ต้องมีความซับซ้อนมากเท่าที่ควร
jrh

1
@jrh: ฉันคิดว่ามันจะค่อนข้างง่าย ยอมรับว่า 1. เพื่อให้นามแฝงเกิดขึ้นระหว่างการเรียกใช้ฟังก์ชันหรือลูปโดยเฉพาะจะต้องใช้พอยน์เตอร์หรือ lvalues ​​ที่ต่างกันสองตัวระหว่างการดำเนินการนั้นเพื่อจัดการกับหน่วยเก็บข้อมูลเดียวกันใน fashon ที่ขัดแย้งกัน 2. ยอมรับว่าในบริบทที่ตัวชี้หรือ lvalue หนึ่งได้มาจากอีกอย่างชัดเจนการเข้าถึงวินาทีนั้นเป็นการเข้าถึงตัวแรก 3. ยอมรับว่ากฎไม่ได้มีวัตถุประสงค์เพื่อใช้ในกรณีที่ไม่เกี่ยวข้องกับนามแฝง
supercat

1
สถานการณ์ที่แน่นอนที่คอมไพเลอร์รับรู้ lvalue ที่ได้มาใหม่อาจเป็นปัญหาคุณภาพของการใช้งาน แต่คอมไพเลอร์ที่เหมาะสมจากระยะไกลใด ๆ ควรจะสามารถจดจำรูปแบบที่ gcc และเสียงดังลั่นโดยเจตนา
supercat

11

หลังจากอ่านคำตอบมากมายฉันรู้สึกว่าต้องเพิ่มบางสิ่ง:

นามแฝงที่เข้มงวด (ซึ่งฉันจะอธิบายเล็กน้อย) มีความสำคัญเนื่องจาก :

  1. การเข้าถึงหน่วยความจำอาจมีราคาแพง (ประสิทธิภาพฉลาด) ซึ่งเป็นเหตุผลที่ข้อมูลถูกจัดการในการลงทะเบียน CPUก่อนที่จะถูกเขียนกลับไปยังหน่วยความจำกายภาพ

  2. หากข้อมูลในการลงทะเบียน 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 ดังนั้นรหัสอาจสิ้นสุดลงเช่นนี้

  1. โหลดaและbจากหน่วยความจำ

  2. เพิ่มไปab

  3. บันทึก bและโหลด a

    (บันทึกจาก CPU register ไปยังหน่วยความจำและโหลดจากหน่วยความจำไปยัง CPU register)

  4. เพิ่มไปba

  5. บันทึกa(จากการลงทะเบียน CPU) ไปยังหน่วยความจำ

ขั้นตอนที่ 3 ช้ามากเพราะต้องการเข้าถึงหน่วยความจำกายภาพ อย่างไรก็ตามจำเป็นต้องป้องกันอินสแตนซ์ที่aและbชี้ไปยังที่อยู่หน่วยความจำเดียวกัน

นามแฝงที่เข้มงวดจะช่วยให้เราสามารถป้องกันสิ่งนี้ได้โดยบอกคอมไพเลอร์ว่าที่อยู่หน่วยความจำเหล่านี้แตกต่างกันอย่างชัดเจน (ซึ่งในกรณีนี้จะช่วยให้การปรับให้เหมาะสมยิ่งขึ้นซึ่งไม่สามารถทำได้

  1. สิ่งนี้สามารถบอกกับคอมไพเลอร์ได้สองวิธีโดยใช้ประเภทที่แตกต่างกันในการชี้ไปที่ เช่น:

    void merge_two_numbers(int *a, long *b) {...}
  2. การใช้restrictคำสำคัญ เช่น:

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

ทีนี้จากการปฏิบัติตามกฎ Strict Aliasing ขั้นตอนที่ 3 สามารถหลีกเลี่ยงได้และโค้ดจะทำงานได้เร็วขึ้นอย่างมีนัยสำคัญ

อันที่จริงแล้วโดยการเพิ่มrestrictคำหลักฟังก์ชันทั้งหมดสามารถปรับให้เหมาะกับ:

  1. โหลดaและbจากหน่วยความจำ

  2. เพิ่มไปab

  3. บันทึกผลลัพธ์ทั้งไปaและbกลับ

การเพิ่มประสิทธิภาพนี้ไม่เคยทำมาก่อนเนื่องจากการชนกันที่เป็นไปได้ (ที่ไหนaและbจะเพิ่มเป็นสามเท่าแทนที่จะเป็นสองเท่า)


ด้วยคำหลักที่ จำกัด ในขั้นตอนที่ 3 ควรบันทึกผลลัพธ์เป็น 'b' เท่านั้นหรือไม่ ดูเหมือนว่าผลลัพธ์ของการรวมจะถูกเก็บไว้ใน 'a' เช่นกัน จำเป็นต้องโหลดใหม่อีกครั้งหรือไม่
NeilB

1
@NeilB - เห่าคุณพูดถูก เราเพียง แต่ประหยัดb(ไม่โหลดมัน) aและโหลด ฉันหวังว่ามันชัดเจนขึ้นตอนนี้
Myst

การใช้นามแฝงแบบพื้นฐานอาจมีประโยชน์บางอย่างมาก่อนrestrictแต่ฉันคิดว่าส่วนใหญ่จะมีประสิทธิภาพมากกว่าและข้อ จำกัด บางประการเกี่ยวกับการregisterอนุญาตให้กรอกในบางกรณีที่restrictไม่สามารถช่วยได้ ฉันไม่แน่ใจว่ามันเป็น "สำคัญ" ต่อการปฏิบัติตามมาตรฐานอย่างเต็มที่ในการอธิบายทุกกรณีที่โปรแกรมเมอร์ควรคาดหวังว่าคอมไพเลอร์จะรับรู้หลักฐานของนามแฝงแทนที่จะอธิบายเพียงสถานที่ที่คอมไพเลอร์ต้องเข้าใจนามแฝงแม้ว่าจะไม่มีหลักฐานใด
supercat

โปรดทราบว่าแม้ว่าการโหลดจาก RAM หลักช้ามาก (และสามารถหยุดการทำงานของคอร์ซีพียูเป็นเวลานานหากการดำเนินการต่อไปนี้ขึ้นอยู่กับผลลัพธ์) การโหลดจากแคช L1 นั้นค่อนข้างเร็วและกำลังเขียนลงในบรรทัดแคชที่เพิ่งเขียน โดยแกนเดียวกัน ดังนั้นทั้งหมดยกเว้นการอ่านหรือเขียนครั้งแรกไปยังที่อยู่มักจะเร็วพอสมควร: ความแตกต่างระหว่างการเข้าถึง reg / mem addr นั้นมีขนาดเล็กกว่าความแตกต่างระหว่างการเพิ่มแคช / uncached mem
curiousguy

@currguy - แม้ว่าคุณจะถูกต้อง "เร็ว" ในกรณีนี้จะสัมพันธ์กัน แคช L1 อาจยังคงมีลำดับความสำคัญช้ากว่าการลงทะเบียน CPU (ฉันคิดว่าช้ากว่า 10 เท่า) นอกจากนี้restrictคำหลักยังลดความเร็วของการดำเนินการให้น้อยที่สุด แต่ยังช่วยลดจำนวนการดำเนินการซึ่งอาจมีความหมาย ... ฉันหมายถึงหลังจากนั้นการดำเนินการที่เร็วที่สุดก็ไม่สามารถใช้งานได้เลย :)
Myst

6

นามแฝงที่เข้มงวดไม่อนุญาตให้ตัวชี้ชนิดต่าง ๆ ไปยังข้อมูลเดียวกัน

บทความนี้จะช่วยให้คุณเข้าใจปัญหาโดยละเอียด


4
คุณสามารถนามแฝงระหว่างการอ้างอิงและระหว่างการอ้างอิงและตัวชี้เช่นกัน ดูการสอนของฉันdbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
ได้รับอนุญาตให้มีประเภทของตัวชี้ที่แตกต่างกันไปยังข้อมูลเดียวกัน ที่นามแฝงที่เข้มงวดเข้ามาคือเมื่อตำแหน่งหน่วยความจำเดียวกันถูกเขียนผ่านตัวชี้ประเภทหนึ่งและอ่านผ่านอีกตำแหน่งหนึ่ง นอกจากนี้ยังอนุญาตบางประเภทที่แตกต่างกัน (เช่นintและโครงสร้างที่มีint)
MM

-3

ในทางเทคนิคใน C ++ กฎนามแฝงที่เข้มงวดนั้นอาจไม่สามารถใช้ได้

หมายเหตุคำจำกัดความของการส่งข้อมูลทางอ้อม ( * โอเปอเรเตอร์ ):

ตัวดำเนินการ unary * ดำเนินการทางอ้อม: นิพจน์ที่ใช้จะเป็นตัวชี้ไปยังชนิดของวัตถุหรือตัวชี้ไปยังประเภทฟังก์ชันและผลลัพธ์คือค่า lvalue ที่อ้างถึงวัตถุหรือฟังก์ชันที่จุดแสดงออกนั้น

นอกจากนี้จากคำจำกัดความของ glvalue

glvalue คือนิพจน์ที่การประเมินผลกำหนดตัวตนของวัตถุ (... snip)

ดังนั้นในการติดตามโปรแกรมที่กำหนดไว้อย่างดี glvalue หมายถึงวัตถุ ดังนั้นกฎนามแฝงที่เข้มงวดจึงไม่มีผลบังคับใช้ นี่อาจไม่ใช่สิ่งที่นักออกแบบต้องการ


4
C Standard ใช้คำว่า "object" เพื่ออ้างถึงแนวคิดที่แตกต่าง ในหมู่พวกเขาลำดับของไบต์ที่จัดสรรให้กับวัตถุประสงค์บางอย่างเท่านั้นการอ้างอิงที่ไม่จำเป็น แต่เพียงผู้เดียวถึงลำดับของไบต์ถึง / ซึ่งค่าของประเภทเฉพาะสามารถเขียนหรืออ่านได้หรือการอ้างอิงที่จริงมี เคยหรือจะสามารถเข้าถึงได้ในบางบริบท ฉันไม่คิดว่าจะมีวิธีที่สมเหตุสมผลในการกำหนดคำว่า "วัตถุ" ที่จะสอดคล้องกับทุกวิธีที่มาตรฐานใช้
supercat

@supercat ไม่ถูกต้อง แม้จะมีจินตนาการของคุณเป็นจริงค่อนข้างสอดคล้อง ใน ISO C มันถูกกำหนดให้เป็น "ภูมิภาคของการจัดเก็บข้อมูลในสภาพแวดล้อมการดำเนินการเนื้อหาที่สามารถเป็นตัวแทนของค่า" ใน ISO C ++ มีคำจำกัดความที่คล้ายกันคือ ความคิดเห็นของคุณนั้นไม่เกี่ยวข้องมากไปกว่าคำตอบเพราะสิ่งที่คุณกล่าวถึงเป็นวิธีการแสดงเนื้อหาอ้างอิงของวัตถุในขณะที่คำตอบนั้นแสดงแนวคิด C ++ (glvalue) ของนิพจน์ที่เกี่ยวข้องกับตัวตนของวัตถุอย่างแน่นหนา และกฎนามแฝงทั้งหมดนั้นเกี่ยวข้องกับตัวตน แต่ไม่ใช่เนื้อหา
FrankHB

1
@FrankHB: ถ้ามีใครประกาศint foo;สิ่งที่เข้าถึงได้โดยการแสดงออก lvalue *(char*)&foo? นั่นเป็นวัตถุประเภทcharใช่หรือไม่ วัตถุนั้นเกิดขึ้นพร้อมกันfooหรือไม่? จะเขียนเพื่อfooเปลี่ยนค่าที่เก็บไว้ของวัตถุประเภทดังกล่าวข้างต้นcharหรือไม่ ถ้าเป็นเช่นนั้นมีกฎใดบ้างที่จะอนุญาตให้ค่าที่เก็บไว้ของวัตถุประเภทนั้นcharสามารถเข้าถึงได้โดยใช้ lvalue ชนิดint?
supercat

@ FrankHB: ในกรณีที่ไม่มี 6.5p7 เราสามารถพูดได้ว่าทุกพื้นที่จัดเก็บพร้อมกันมีวัตถุทั้งหมดทุกประเภทที่สามารถพอดีกับพื้นที่เก็บข้อมูลนั้นและการเข้าถึงพื้นที่เก็บข้อมูลนั้นเข้าถึงพื้นที่ทั้งหมดได้พร้อมกัน การตีความในลักษณะนี้การใช้คำว่า "object" ใน 6.5p7 อย่างไรก็ตามจะห้ามทำสิ่งใดมากกับ lvalues ​​ที่ไม่ใช่ตัวอักษรซึ่งจะเป็นผลลัพธ์ที่ไร้สาระและเอาชนะวัตถุประสงค์ของกฎโดยสิ้นเชิง นอกจากนี้แนวคิดของ "วัตถุ" ที่ใช้ทุกที่นอกเหนือจาก 6.5p6 มีประเภทเวลารวบรวมแบบคงที่ แต่ ...
supercat

1
sizeof (int) คือ 4, ไม่ประกาศ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"
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.