เหตุใดคอมไพเลอร์ C ++ จึงไม่ปรับการกำหนดบูลีนแบบมีเงื่อนไขนี้ให้เหมาะสมเป็นการมอบหมายแบบไม่มีเงื่อนไข


117

พิจารณาฟังก์ชันต่อไปนี้:

void func(bool& flag)
{
    if(!flag) flag=true;
}

สำหรับฉันแล้วดูเหมือนว่าหากแฟล็กมีค่าบูลีนที่ถูกต้องสิ่งนี้จะเทียบเท่ากับการตั้งค่าโดยไม่มีเงื่อนไขtrueดังนี้:

void func(bool& flag)
{
    flag=true;
}

ทั้ง gcc หรือ clang ไม่ได้ปรับให้เหมาะสมด้วยวิธีนี้ - ทั้งคู่สร้างสิ่งต่อไปนี้ที่-O3ระดับการเพิ่มประสิทธิภาพ:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

คำถามของฉันคือรหัสเป็นกรณีพิเศษเกินกว่าที่จะดูแลเพื่อเพิ่มประสิทธิภาพหรือมีเหตุผลที่ดีที่ทำให้การเพิ่มประสิทธิภาพดังกล่าวไม่เป็นที่ต้องการเนื่องจากflagไม่ได้อ้างอิงถึงvolatile? ดูเหมือนว่าเหตุผลเดียวที่อาจเป็นไปได้คือflagอาจมีค่าที่ไม่ใช่trueหรือfalseมูลค่าโดยไม่มีพฤติกรรมที่ไม่ได้กำหนดเมื่ออ่าน แต่ฉันไม่แน่ใจว่าเป็นไปได้หรือไม่


8
คุณมีหลักฐานว่าเป็นการ "เพิ่มประสิทธิภาพ" หรือไม่?
David Schwartz

1
@ 200_success ฉันไม่คิดว่าการใส่บรรทัดของโค้ดที่มีมาร์กอัปที่ใช้งานไม่ได้เป็นชื่อเป็นเรื่องที่ดี หากคุณต้องการชื่อเรื่องที่เฉพาะเจาะจงมากขึ้นแต่ให้เลือกประโยคภาษาอังกฤษและพยายามหลีกเลี่ยงรหัสในนั้น (เช่นทำไมคอมไพเลอร์ไม่ปรับการเขียนแบบมีเงื่อนไขให้เหมาะสมกับการเขียนแบบไม่มีเงื่อนไขเมื่อสามารถพิสูจน์ได้ว่าเทียบเท่าหรือคล้ายกัน) เนื่องจากไม่มีการแสดงผล backticks อย่าใช้ในชื่อแม้ว่าคุณจะใช้รหัสก็ตาม
Bakuriu

2
@Ruslan ในขณะที่ดูเหมือนจะไม่ทำการปรับให้เหมาะสมกับฟังก์ชันนี้เอง แต่เมื่อสามารถแทรกโค้ดได้ แต่ดูเหมือนว่าจะทำเช่นนั้นสำหรับเวอร์ชันอินไลน์ มักจะส่งผลให้ค่าคงที่เวลาคอมไพล์1ถูกใช้ godbolt.org/g/swe0tc
Evan Teran

คำตอบ:


102

สิ่งนี้อาจส่งผลเสียต่อประสิทธิภาพการทำงานของโปรแกรมเนื่องจากการพิจารณาการเชื่อมโยงกันของแคช การเขียนถึงflagแต่ละครั้งfunc()จะทำให้บรรทัดแคชที่มีอยู่สกปรก สิ่งนี้จะเกิดขึ้นไม่ว่าค่าที่เขียนจะตรงกับบิตที่พบในแอดเดรสปลายทางก่อนการเขียนก็ตาม


แก้ไข

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

หลังจากไตร่ตรองอีกเล็กน้อยฉันสามารถเสนออีกหนึ่งตัวอย่างว่าทำไมคอมไพเลอร์จึงควรถูกแบนอย่างยิ่ง - เว้นแต่จะพิสูจน์ได้ว่าการเปลี่ยนแปลงนั้นปลอดภัยสำหรับบริบทเฉพาะ - จากการแนะนำการเขียนที่ไม่มีเงื่อนไข พิจารณารหัสนี้:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

ด้วยการเขียนแบบไม่มีเงื่อนไขในfunc()สิ่งนี้จะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดไว้อย่างแน่นอน (การเขียนลงในหน่วยความจำแบบอ่านอย่างเดียวจะยุติโปรแกรมแม้ว่าผลของการเขียนจะเป็นแบบไม่ต้องดำเนินการก็ตาม)


7
นอกจากนี้ยังอาจส่งผลในเชิงบวกต่อประสิทธิภาพเนื่องจากคุณกำจัดสาขาออกไป ดังนั้นฉันไม่คิดว่ากรณีเฉพาะนี้มีความหมายที่จะพูดคุยโดยไม่มีระบบที่เฉพาะเจาะจงมากนัก
Lundin

3
การกำหนดพฤติกรรม @Yakk ไม่ได้รับผลกระทบจากแพลตฟอร์มเป้าหมาย การบอกว่าจะยุติโปรแกรมนั้นไม่ถูกต้อง แต่ UB เองก็อาจส่งผลที่ตามมามากมายรวมถึงปีศาจจมูก
John Dvorak

16
@Yakk ขึ้นอยู่กับความหมายของ "หน่วยความจำแบบอ่านอย่างเดียว" ไม่มันไม่ได้อยู่ในชิป ROM แต่มักอยู่ในส่วนที่โหลดไปยังหน้าที่ไม่ได้เปิดใช้งานการเข้าถึงการเขียนและคุณจะได้รับเช่นสัญญาณ SIGSEGV หรือข้อยกเว้น STATUS_ACCESS_VIOLATION เมื่อคุณพยายามเขียนลงไป
Random832

5
"สิ่งนี้ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนดอย่างแน่นอน" ไม่พฤติกรรมที่ไม่ได้กำหนดเป็นคุณสมบัติของเครื่องจักรนามธรรม เป็นรหัสที่ระบุว่ามี UB อยู่หรือไม่ คอมไพเลอร์ไม่สามารถทำให้เกิดได้ (แม้ว่าคอมไพเลอร์บั๊กอาจทำให้โปรแกรมทำงานไม่ถูกต้อง)
Eric M Schmidt

7
เป็นการconstส่งผ่านไปยังฟังก์ชันที่อาจแก้ไขข้อมูลที่เป็นแหล่งที่มาของพฤติกรรมที่ไม่ได้กำหนดไม่ใช่การเขียนโดยไม่มีเงื่อนไข หมอเจ็บมากเมื่อทำแบบนี้ ....
Spencer

48

นอกเหนือจากคำตอบของ Leon ในเรื่องประสิทธิภาพ:

สมมติว่าflagเป็นtrue. func(flag)สมมติว่าสองหัวข้อจะเรียกร้องอย่างต่อเนื่อง ฟังก์ชันตามที่เขียนในกรณีนั้นจะไม่เก็บสิ่งใดไว้flagดังนั้นสิ่งนี้ควรปลอดภัยสำหรับเธรด สองเธรดเข้าถึงหน่วยความจำเดียวกัน แต่จะอ่านได้เท่านั้น โดยไม่มีเงื่อนไขการตั้งค่าflagที่จะtrueหมายถึงสองหัวข้อที่แตกต่างกันจะเขียนหน่วยความจำเดียวกัน สิ่งนี้ไม่ปลอดภัยไม่ปลอดภัยแม้ว่าข้อมูลที่เขียนจะเหมือนกับข้อมูลที่มีอยู่แล้วก็ตาม


9
ฉันคิดว่า[intro.races]/21นี้เป็นผลมาจากการใช้
Griwes

10
น่าสนใจมาก. ดังนั้นฉันจึงอ่านว่า: คอมไพเลอร์ไม่ได้รับอนุญาตให้ "เพิ่มประสิทธิภาพใน" การดำเนินการเขียนโดยที่เครื่องนามธรรมไม่มี
มาร์ตินบา

3
@MartinBa ส่วนใหญ่แล้ว แต่ถ้าคอมไพลเลอร์สามารถพิสูจน์ได้ว่ามันไม่สำคัญเช่นเพราะสามารถพิสูจน์ได้ว่าไม่มีเธรดอื่นที่สามารถเข้าถึงตัวแปรนั้น ๆ ได้ก็อาจจะไม่เป็นไร

13
นี้เป็นเพียงไม่ปลอดภัยถ้าระบบคอมไพเลอร์ที่มีการกำหนดเป้าหมายที่จะทำให้มันไม่ปลอดภัย ฉันไม่เคยพัฒนาระบบที่การเขียน0x01เป็นไบต์ซึ่ง0x01ทำให้เกิดพฤติกรรม "ไม่ปลอดภัย" อยู่แล้ว ในระบบที่มีการเข้าถึงหน่วยความจำ word หรือ dword มันจะ; แต่เครื่องมือเพิ่มประสิทธิภาพควรตระหนักถึงสิ่งนี้ บนพีซีหรือระบบปฏิบัติการโทรศัพท์ที่ทันสมัยจะไม่มีปัญหาเกิดขึ้น นี่ไม่ใช่เหตุผลที่ถูกต้อง
Yakk - Adam Nevraumont

4
@Yakk ที่จริงคิดให้มากกว่านี้ฉันคิดว่ามันถูกต้องแล้วแม้กระทั่งสำหรับโปรเซสเซอร์ทั่วไป ฉันคิดว่าคุณพูดถูกเมื่อ CPU สามารถเขียนไปยังหน่วยความจำได้โดยตรง แต่สมมติว่าflagอยู่ในหน้า copy-on-write ตอนนี้ที่ระดับ CPU อาจมีการกำหนดลักษณะการทำงาน (page fault ปล่อยให้ OS จัดการ) แต่ในระดับ OS อาจยังไม่ได้กำหนดใช่ไหม

13

ฉันไม่แน่ใจเกี่ยวกับลักษณะการทำงานของ C ++ ที่นี่ แต่ใน C หน่วยความจำอาจเปลี่ยนไปเนื่องจากหากหน่วยความจำมีค่าที่ไม่ใช่ศูนย์อื่นที่ไม่ใช่ 1 หน่วยความจำนั้นจะยังคงไม่เปลี่ยนแปลงกับการตรวจสอบ แต่เปลี่ยนเป็น 1 ด้วยการตรวจสอบ

แต่เนื่องจากฉันไม่ค่อยชำนาญใน C ++ ฉันจึงไม่รู้ว่าสถานการณ์นี้จะเป็นไปได้หรือไม่


สิ่งนี้จะยังคงเป็นจริง_Boolหรือไม่?
Ruslan

5
ใน C หากหน่วยความจำมีค่าที่ ABI ไม่ได้บอกว่าถูกต้องสำหรับประเภทของมันแสดงว่าเป็นการแสดงกับดักและการอ่านการแทนกับดักถือเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ ใน C ++ สิ่งนี้จะเกิดขึ้นได้ก็ต่อเมื่ออ่านวัตถุที่ไม่ได้เริ่มต้นและอ่านวัตถุที่ไม่ได้เริ่มต้นนั่นคือ UB แต่ถ้าคุณสามารถหา ABI ที่ระบุว่าค่าใด ๆ ที่ไม่ใช่ศูนย์นั้นใช้ได้สำหรับประเภทbool/ _Boolและค่าเฉลี่ยtrueใน ABI นั้นคุณก็น่าจะถูก

1
@Ruslan ด้วยคอมไพเลอร์ที่ใช้ Itanium ABI และบนโปรเซสเซอร์ ARM C _Boolและ C ++ boolเป็นประเภทเดียวกันหรือประเภทที่เข้ากันได้ซึ่งเป็นไปตามกฎเดียวกัน MSVC มีขนาดและแนวเดียวกัน แต่ไม่มีคำชี้แจงอย่างเป็นทางการว่าใช้กฎเดียวกันหรือไม่
Justin Time - คืนสถานะ Monica

1
@JustinTime: C <stdbool.h>รวม a typedef _Bool bool; และใช่บน x86 (อย่างน้อยใน System V ABI) bool/ _Boolต้องเป็น 0 หรือ 1 โดยที่บิตด้านบนของไบต์จะถูกล้าง ฉันไม่คิดว่าคำอธิบายนี้เป็นไปได้
Peter Cordes

1
@JustinTime: นั่นเป็นความจริงฉันควรจะชี้ให้เห็นว่ามันมีความหมายเหมือนกันในรสชาติ x86 ทั้งหมดของ System V ABI ซึ่งเป็นคำถามเกี่ยวกับ (ฉันสามารถบอกได้เพราะอาร์กิวเมนต์แรกถึงfuncถูกส่งผ่านใน RDI ในขณะที่ Windows จะใช้ RDX)
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.