โอเวอร์โฟลว์ที่มีการลงชื่อใน C ++ และพฤติกรรมที่ไม่ได้กำหนด (UB)


56

ฉันสงสัยเกี่ยวกับการใช้รหัสดังต่อไปนี้

int result = 0;
int factor = 1;
for (...) {
    result = ...
    factor *= 10;
}
return result;

หากวนซ้ำซ้ำหลายnครั้งfactorจะถูกคูณด้วยจำนวนเวลาที่10แน่นอน nอย่างไรก็ตามfactorมีการใช้งานเฉพาะหลังจากที่ถูกคูณด้วย10จำนวนn-1ครั้งทั้งหมด หากเราสมมติว่าfactorไม่มีการโอเวอร์โฟลว์ยกเว้นในการวนซ้ำครั้งสุดท้ายของลูป แต่อาจโอเวอร์โฟลว์ในการวนซ้ำครั้งสุดท้ายของลูปรหัสนั้นควรยอมรับได้หรือไม่ ในกรณีนี้ค่าของfactorจะไม่ถูกใช้อย่างแน่นอนหลังจากเกิดการล้น

ฉันกำลังถกเถียงกันว่าควรยอมรับรหัสเช่นนี้หรือไม่ มันจะเป็นไปได้ที่จะนำการคูณเข้ามาในคำสั่ง if และเพียงแค่ไม่ทำการคูณในการวนซ้ำครั้งสุดท้ายของลูปเมื่อมันสามารถล้น ข้อเสียคือมัน clutters รหัสและเพิ่มสาขาที่ไม่จำเป็นที่จะต้องตรวจสอบในการวนซ้ำก่อนหน้านี้ทั้งหมด ฉันยังสามารถวนซ้ำวนซ้ำในเวลาที่น้อยลงและทำซ้ำลูปบอดี้อีกครั้งหลังจากวนซ้ำอีกครั้งนี่ทำให้โค้ดซับซ้อน

มีการใช้รหัสจริงในวงในที่แน่นหนาซึ่งใช้เวลาในการประมวลผลของ CPU ทั้งหมดในแอปพลิเคชันกราฟิกแบบเรียลไทม์


5
ฉันโหวตให้ปิดคำถามนี้เป็นนอกหัวข้อเนื่องจากคำถามนี้ควรอยู่ในcodereview.stackexchange.comไม่ได้อยู่ที่นี่
Kevin Anderson

31
@KevinAnderson ไม่ถูกต้องที่นี่เนื่องจากตัวอย่างรหัสจะได้รับการแก้ไขไม่ใช่แค่ปรับปรุง
Bathsheba

1
@harold พวกเขากำลังปิด
Lightness Races ที่ Orbit

1
@LightnessRaceswithMonica: ผู้เขียนมาตรฐานตั้งใจและคาดว่าการใช้งานที่มีไว้สำหรับแพลตฟอร์มและวัตถุประสงค์ต่างๆจะขยายความหมายที่มีให้โปรแกรมเมอร์โดยการประมวลผลการกระทำต่าง ๆ ในลักษณะที่เป็นประโยชน์สำหรับแพลตฟอร์มเหล่านั้นหรือไม่หรือไม่มาตรฐานต้องการให้พวกเขาทำหรือไม่ และยังระบุด้วยว่าพวกเขาไม่ต้องการลบรหัสที่ไม่ใช่แบบพกพา ดังนั้นความคล้ายคลึงกันระหว่างคำถามขึ้นอยู่กับการใช้งานที่ต้องการการสนับสนุน
supercat

2
@supercat สำหรับพฤติกรรมที่กำหนดการใช้งานแน่นอนและถ้าคุณรู้ว่า toolchain ของคุณมีส่วนขยายบางส่วนคุณสามารถใช้ (และคุณไม่สนใจเรื่องความสะดวกในการพกพา) ได้ สำหรับ UB? น่าสงสัย
การแข่งขัน Lightness ใน Orbit

คำตอบ:


51

คอมไพเลอร์สมมติว่าโปรแกรม C ++ ที่ถูกต้องไม่มี UB ลองพิจารณาตัวอย่าง:

if (x == nullptr) {
    *x = 3;
} else {
    *x = 5;
}

ถ้า x == nullptrนั้นการลงทะเบียนและการกำหนดค่าคือ UB ดังนั้นวิธีเดียวที่สิ่งนี้อาจจบลงในโปรแกรมที่ถูกต้องคือเมื่อx == nullptrจะไม่มีผลตอบแทนจริงและคอมไพเลอร์สามารถถือว่าภายใต้กฎราวกับว่ากฎดังกล่าวข้างต้นเทียบเท่ากับ:

*x = 5;

ตอนนี้ในรหัสของคุณ

int result = 0;
int factor = 1;
for (...) {      // Loop until factor overflows but not more
   result = ...
   factor *= 10;
}
return result;

การคูณครั้งสุดท้ายของfactorไม่สามารถเกิดขึ้นได้ในโปรแกรมที่ถูกต้อง (การโอเวอร์โฟลว์ที่ลงชื่อแล้วไม่ได้กำหนดไว้) ดังนั้นการกำหนดให้resultไม่สามารถเกิดขึ้นได้ เนื่องจากไม่มีวิธีการแยกสาขาก่อนการทำซ้ำครั้งล่าสุดและการทำซ้ำครั้งก่อนไม่สามารถเกิดขึ้นได้ ในที่สุดส่วนของรหัสที่ถูกต้อง (เช่นไม่มีพฤติกรรมที่ไม่ได้กำหนดเกิดขึ้น) คือ:

// nothing :(

6
"พฤติกรรมที่ไม่ได้กำหนด" เป็นนิพจน์ที่เราได้ยินมากในคำตอบ SO โดยไม่อธิบายอย่างชัดเจนว่ามันสามารถส่งผลกระทบต่อโปรแกรมโดยรวมได้อย่างไร คำตอบนี้ทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้น
Gilles-Philippe Paillé

1
และนี่อาจเป็น "การเพิ่มประสิทธิภาพที่มีประโยชน์" ถ้าฟังก์ชั่นถูกเรียกใช้เฉพาะเป้าหมายที่มีINT_MAX >= 10000000000ฟังก์ชันอื่นที่เรียกว่าในกรณีที่INT_MAXมีขนาดเล็กกว่า
.. GitHub หยุดช่วย ICE

2
@ Gilles-PhilippePailléมีหลายครั้งที่ฉันหวังว่าเราจะได้โพสต์ข้อความนั้น การแข่งขัน Data Benignเป็นหนึ่งในรายการโปรดของฉันสำหรับการจับภาพว่าน่ารังเกียจเพียงใด นอกจากนี้ยังมีรายงานข้อผิดพลาดที่ยอดเยี่ยมใน MySQL ที่ฉันไม่สามารถหาได้อีก - การตรวจสอบบัฟเฟอร์ล้นที่เรียกใช้ UB โดยไม่ตั้งใจ เวอร์ชั่นเฉพาะของคอมไพเลอร์โดยเฉพาะสันนิษฐานว่า UB ไม่เคยเกิดขึ้นและปรับแต่งการตรวจสอบโอเวอร์โฟลว์ทั้งหมด
Cort Ammon

1
@SolomonSlow: สถานการณ์หลักที่ UB ขัดแย้งกันคือสถานการณ์ที่ส่วนของเอกสารมาตรฐานและการใช้งานอธิบายถึงพฤติกรรมของการกระทำบางอย่าง แต่อีกส่วนหนึ่งของมาตรฐานนั้นมีลักษณะเป็น UB การปฏิบัติร่วมกันก่อนที่มาตรฐานจะถูกเขียนขึ้นเพื่อให้ผู้เขียนคอมไพเลอร์ประมวลผลการกระทำดังกล่าวอย่างมีความหมายยกเว้นเมื่อลูกค้าของพวกเขาจะได้รับประโยชน์จากการทำสิ่งอื่นและฉันไม่คิดว่าผู้เขียนมาตรฐานจะจินตนาการว่าผู้เขียนคอมไพเลอร์ .
supercat

2
@ Gilles-PhilippePaillé: สิ่งที่โปรแกรมเมอร์ C ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดจากบล็อก LLVM ก็ดีเช่นกัน มันอธิบายถึงวิธีการยกตัวอย่างเช่นล้น - จำนวนเต็ม UB สามารถให้คอมไพเลอร์พิสูจน์ว่าi <= nลูปมักจะไม่สิ้นสุดเช่นi<nลูป และเลื่อนint iไปที่ความกว้างของตัวชี้ในลูปแทนที่จะต้องทำเครื่องหมายซ้ำเพื่อทำดัชนีอาเรย์ห่อที่เป็นไปได้สำหรับองค์ประกอบอาเรย์ 4G ตัวแรก
Peter Cordes

34

พฤติกรรมของintล้นไม่ได้กำหนดไว้

มันไม่สำคัญว่าคุณจะอ่านfactorนอกร่างกายของลูปหรือไม่ ถ้ามันล้นด้วยแล้วพฤติกรรมของรหัสของคุณในหลังจากและค่อนข้างขัดแย้งกันมาก่อนล้นไม่ได้กำหนด

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

คุณไม่สามารถใช้unsignedประเภทสำหรับfactorถึงแม้ว่าคุณจะต้องกังวลเกี่ยวกับการแปลงที่ไม่ต้องการintเป็นunsignedในนิพจน์ที่มีทั้งสองอย่างใช่ไหม


12
@nicomp; ทำไมจะไม่ล่ะ?
Bathsheba

12
@ Gilles-PhilippePaillé: คำตอบของฉันไม่ได้บอกคุณว่ามันเป็นปัญหาหรือไม่? ประโยคเปิดของฉันไม่จำเป็นต้องมีสำหรับ OP แต่เป็นชุมชนที่กว้างขึ้นและfactor"ถูกใช้" ในการมอบหมายกลับสู่ตัวของมันเอง
Bathsheba

8
@ Gilles-PhilippePailléและคำตอบนี้อธิบายว่าทำไมจึงเป็นปัญหา
idclev 463035818

1
@Bathsheba คุณพูดถูกฉันเข้าใจผิดคำตอบของคุณ
Gilles-Philippe Paillé

4
เป็นตัวอย่างของพฤติกรรมที่ไม่ได้กำหนดไว้เมื่อโค้ดนั้นถูกคอมไพล์ด้วยการเปิดใช้งานการตรวจสอบรันไทม์มันจะยุติแทนที่จะส่งคืนผลลัพธ์ รหัสที่ต้องให้ฉันปิดฟังก์ชั่นการวินิจฉัยเพื่อที่จะใช้งานไม่ได้
Simon Richter

23

อาจเป็นเรื่องลึกซึ้งที่จะต้องพิจารณาตัวเพิ่มประสิทธิภาพที่แท้จริง การคลายลูปเป็นเทคนิคที่รู้จักกันดี แนวคิดพื้นฐานเกี่ยวกับ op unrolling คือ

for (int i = 0; i != 3; ++i)
    foo()

อาจใช้งานได้ดีกว่าเบื้องหลังเช่น

 foo()
 foo()
 foo()

นี่เป็นกรณีง่าย ๆ ที่มีขอบเขตคงที่ แต่คอมไพเลอร์สมัยใหม่ยังสามารถทำเช่นนี้เพื่อขอบเขตตัวแปร:

for (int i = 0; i != N; ++i)
   foo();

กลายเป็น

__RELATIVE_JUMP(3-N)
foo();
foo();
foo();

เห็นได้ชัดว่าวิธีนี้ใช้ได้เฉพาะในกรณีที่คอมไพเลอร์รู้ว่า N <= 3 และนั่นคือสิ่งที่เรากลับไปที่คำถามเดิม เนื่องจากคอมไพเลอร์รู้ว่าการโอเวอร์โฟลว์ที่ลงนามแล้วไม่เกิดขึ้นจึงรู้ว่าลูปสามารถดำเนินการได้สูงสุด 9 ครั้งในสถาปัตยกรรม 32 บิต 10^10 > 2^32. ดังนั้นจึงสามารถทำซ้ำวนซ้ำได้ 9 อัน แต่ความตั้งใจสูงสุดคือ 10 ซ้ำ! .

สิ่งที่อาจเกิดขึ้นคือคุณได้รับการกระโดดแบบสัมพัทธ์กับคำสั่งชุดประกอบ (9-N) ด้วย N = 10 ดังนั้นออฟเซ็ตของ -1 ซึ่งเป็นคำสั่งการกระโดดเอง อุ่ย นี่คือการเพิ่มประสิทธิภาพลูปที่ถูกต้องสมบูรณ์แบบสำหรับ C ++ ที่กำหนดไว้อย่างดี แต่ตัวอย่างที่ให้กลายเป็นลูปไม่ จำกัด


9

การโอเวอร์โฟลว์จำนวนเต็มที่ลงนามใด ๆ ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดไม่ว่าจะมีการอ่านค่าที่มากเกินไปหรือไม่ก็ตาม

บางทีในกรณีใช้งานของคุณคุณสามารถยกการวนซ้ำครั้งแรกออกจากลูปได้

int result = 0;
int factor = 1;
for (int n = 0; n < 10; ++n) {
    result += n + factor;
    factor *= 10;
}
// factor "is" 10^10 > INT_MAX, UB

ลงในนี้

int factor = 1;
int result = 0 + factor; // first iteration
for (int n = 1; n < 10; ++n) {
    factor *= 10;
    result += n + factor;
}
// factor is 10^9 < INT_MAX

ด้วยการเปิดใช้งานการปรับให้เหมาะสมคอมไพเลอร์อาจคลี่วงที่สองด้านบนลงในการกระโดดแบบมีเงื่อนไขหนึ่งครั้ง


6
นี่อาจเป็นเพียงเล็กน้อยเกินเทคนิค แต่ "ล้นล้นลงนามคือพฤติกรรมที่ไม่ได้กำหนด" เป็น oversimplified พฤติกรรมของโปรแกรมที่มี overflow ที่ลงชื่อแล้วไม่ได้ถูกกำหนดไว้ นั่นคือมาตรฐานไม่ได้บอกคุณว่าโปรแกรมทำอะไร ไม่ใช่ว่ามีบางอย่างผิดปกติกับผลลัพธ์ที่ล้น มีบางอย่างผิดปกติกับโปรแกรมทั้งหมด
Pete Becker

การสังเกตอย่างยุติธรรมฉันได้แก้ไขคำตอบของฉันแล้ว
elbrunovsky

หรือมากกว่านั้นให้ทำซ้ำครั้งสุดท้ายแล้วเอาคนตายออกไปfactor *= 10;
Peter Cordes

9

นี่คือ UB; ในเงื่อนไข ISO C ++ พฤติกรรมทั้งหมดของโปรแกรมทั้งหมดนั้นไม่ได้ระบุอย่างสมบูรณ์สำหรับการดำเนินการที่ในที่สุดก็กระทบ UB ตัวอย่างคลาสสิกนั้นไกลถึงมาตรฐาน C ++ มันสามารถทำให้ปีศาจบินออกจากจมูกของคุณได้ (ฉันแนะนำไม่ให้ใช้การนำไปปฏิบัติในกรณีที่ปีศาจในจมูกเป็นไปได้จริง) ดูคำตอบอื่น ๆ สำหรับรายละเอียดเพิ่มเติม

คอมไพเลอร์สามารถ "ทำให้เกิดปัญหา" ณ เวลารวบรวมสำหรับเส้นทางของการดำเนินการที่พวกเขาสามารถเห็นได้ว่านำไปสู่การรวบรวม UB ที่มองเห็นได้ในเวลารวบรวมเช่นสมมติว่าบล็อกพื้นฐานเหล่านั้นจะไม่ถึง

ดูสิ่งที่โปรแกรมเมอร์ C ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด (บล็อก LLVM) ตามที่ได้อธิบายมีการลงนามล้น UB ช่วยให้คอมไพเลอร์จะพิสูจน์ว่าลูปลูปไม่ได้ไม่มีที่สิ้นสุดแม้ไม่รู้จักfor(... i <= n ...) nนอกจากนี้ยังช่วยให้พวกเขา "ส่งเสริม" เคาน์เตอร์วนรอบ int ถึงความกว้างของตัวชี้แทนการทำซ้ำส่วนขยายสัญญาณ (ดังนั้นผลที่ตามมาของ UB ในกรณีนั้นอาจเป็นการเข้าถึงภายนอกองค์ประกอบ 64k หรือ 4G ที่ต่ำของอาเรย์ถ้าคุณคาดว่าจะมีการตัดคำของiในช่วงค่าของมัน)

ในบางกรณีคอมไพเลอร์จะส่งคำสั่งที่ผิดกฎหมายเช่น x86 ud2สำหรับบล็อกที่พิสูจน์ได้ว่าเป็นสาเหตุของ UB หากเคยถูกประหารชีวิต (โปรดทราบว่าอาจไม่มีการเรียกใช้ฟังก์ชั่นดังนั้นคอมไพเลอร์จึงไม่สามารถใช้งานได้โดยทั่วไปและทำลายฟังก์ชั่นอื่น ๆ หรือแม้แต่เส้นทางที่เป็นไปได้ผ่านฟังก์ชั่นที่ไม่ได้กด UB เช่นรหัสเครื่องจักรที่คอมไพล์ อินพุตทั้งหมดที่ไม่นำไปสู่ ​​UB)


อาจเป็นวิธีที่มีประสิทธิภาพมากที่สุดคือการลอกแบบวนซ้ำล่าสุดด้วยตนเองเพื่อfactor*=10หลีกเลี่ยงการใช้ที่ไม่จำเป็น

int result = 0;
int factor = 1;
for (... i < n-1) {   // stop 1 iteration early
    result = ...
    factor *= 10;
}
 result = ...      // another copy of the loop body, using the last factor
 //   factor *= 10;    // and optimize away this dead operation.
return result;

หรือถ้าร่างกายของวงที่มีขนาดใหญ่พิจารณาเพียงแค่ใช้ชนิดที่ได้รับการรับรองสำหรับ factor จากนั้นคุณสามารถปล่อยให้การคูณทวีคูณที่ไม่ได้ลงนามและมันจะทำการตัดที่กำหนดไว้อย่างดีกับกำลังของ 2 (จำนวนของค่าบิตในประเภทที่ไม่ได้ลงนาม)

สิ่งนี้จะใช้ได้แม้ว่าคุณจะใช้กับประเภทที่เซ็นชื่อแล้วโดยเฉพาะอย่างยิ่งหากการแปลงที่ไม่ได้ลงนาม -> ที่ลงนามแล้วจะไม่ล้น

การแปลงระหว่างส่วนเสริมที่ไม่ได้ลงนามและส่วนเสริม 2 นั้นฟรี (รูปแบบบิตเดียวกันสำหรับค่าทั้งหมด); การห่อโมดูโลสำหรับ int -> ที่ไม่ได้ลงนามซึ่งระบุโดยมาตรฐาน C ++ ช่วยให้ง่ายขึ้นเพียงแค่ใช้รูปแบบบิตเดียวกันซึ่งต่างจากส่วนประกอบหรือเครื่องหมาย / ขนาด

และ unsigned-> INT_MAXลงนามเป็นที่น่ารำคาญในทำนองเดียวกันแม้ว่ามันคือการดำเนินการกำหนดค่าที่มีขนาดใหญ่กว่า หากคุณไม่ได้ใช้ผลลัพธ์ที่ไม่ได้ลงชื่อจำนวนมากจากการทำซ้ำครั้งล่าสุดคุณไม่มีอะไรต้องกังวล แต่ถ้าคุณเป็นโปรดดูการแปลงจากไม่ได้รับการลงนามเป็นไม่ได้ลงนามหรือไม่? . ตัวพิมพ์เล็ก - ใหญ่ไม่เหมาะสมกับการใช้งานซึ่งกำหนดไว้ซึ่งหมายความว่าการใช้งานต้องเลือกพฤติกรรมบางอย่าง คนที่มีสติเพียงแค่ตัดทอน (ถ้าจำเป็น) รูปแบบบิตที่ไม่ได้ลงชื่อและใช้เป็นรูปแบบลายเซ็นเพราะใช้กับค่าที่อยู่ในช่วงแบบเดียวกับที่ไม่มีงานพิเศษ และแน่นอนว่าไม่ใช่ UB ดังนั้นค่าที่ไม่ได้ลงนามขนาดใหญ่สามารถกลายเป็นจำนวนเต็มลบได้ เช่นหลังจากint x = u; gcc และ clang อย่าปรับให้เหมาะสมx>=0เป็นความจริงเสมอแม้ไม่มี-fwrapvเพราะพวกเขากำหนดพฤติกรรม


2
ฉันไม่เข้าใจ downvote ที่นี่ ฉันต้องการโพสต์เกี่ยวกับการลอกการทำซ้ำครั้งล่าสุด แต่เพื่อที่จะตอบคำถามฉันก็โยนประเด็นบางอย่างเกี่ยวกับวิธีห้อมล้อม UB ดูคำตอบอื่น ๆ สำหรับรายละเอียดเพิ่มเติม
Peter Cordes

5

หากคุณสามารถทนต่อคำแนะนำเพิ่มเติมในแอสเซมบลีไม่กี่แทน

int factor = 1;
for (int j = 0; j < n; ++j) {
    ...
    factor *= 10;
}

คุณสามารถเขียน:

int factor = 0;
for (...) {
    factor = 10 * factor + !factor;
    ...
}

เพื่อหลีกเลี่ยงการคูณครั้งล่าสุด !factorจะไม่แนะนำสาขา:

    xor     ebx, ebx
L1:                       
    xor     eax, eax              
    test    ebx, ebx              
    lea     edx, [rbx+rbx*4]      
    sete    al    
    add     ebp, 1                
    lea     ebx, [rax+rdx*2]      
    mov     edi, ebx              
    call    consume(int)          
    cmp     r12d, ebp             
    jne     .L1                   

รหัสนี้

int factor = 0;
for (...) {
    factor = factor ? 10 * factor : 1;
    ...
}

ยังส่งผลให้แอสเซมบลีที่ไม่มีสาขาหลังจากการเพิ่มประสิทธิภาพ

    mov     ebx, 1
    jmp     .L1                   
.L2:                               
    lea     ebx, [rbx+rbx*4]       
    add     ebx, ebx
.L1:
    mov     edi, ebx
    add     ebp, 1
    call    consume(int)
    cmp     r12d, ebp
    jne     .L2

(รวบรวมด้วย GCC 8.3.0 -O3)


1
ง่ายกว่าที่จะลอกการวนซ้ำครั้งล่าสุดเว้นแต่ว่าตัวลูปมีขนาดใหญ่ นี่เป็นแฮ็คที่ฉลาด แต่เพิ่มความหน่วงแฝงของห่วงโซ่การพึ่งพาแบบวนรอบที่ดำเนินการผ่านfactorเล็กน้อย หรือไม่: เมื่อรวบรวมเพื่อ 2x LEA มันเป็นเพียงเกี่ยวกับการเป็นที่มีประสิทธิภาพหน่วยงาน LEA + เพิ่มที่จะทำf *= 10ตามที่f*5*2มีแฝงซ่อนอยู่โดยคนแรกtest LEAแต่มันมีราคาสูงเกิน uops ภายในลูปดังนั้นจึงมีข้อเสียของท
Peter Cordes

4

คุณไม่ได้แสดงสิ่งที่อยู่ในวงเล็บของ forคำสั่ง แต่ฉันจะถือว่ามันเป็นเช่นนี้:

for (int n = 0; n < 10; ++n) {
    result = ...
    factor *= 10;
}

คุณสามารถย้ายการเพิ่มตัวนับและการตรวจสอบการยกเลิกลูปเข้าในร่างกาย:

for (int n = 0; ; ) {
    result = ...
    if (++n >= 10) break;
    factor *= 10;
}

จำนวนคำแนะนำการประกอบในลูปจะยังคงเหมือนเดิม

แรงบันดาลใจจากการนำเสนอของ Andrei Alexandrescu นำเสนอ "Speed ​​Is Found in Minds of People"


2

พิจารณาฟังก์ชั่น:

unsigned mul_mod_65536(unsigned short a, unsigned short b)
{
  return (a*b) & 0xFFFFu;
}

อ้างอิงถึงเหตุผลการตีพิมพ์เขียนของมาตรฐานที่คาดว่าจะมีว่าถ้าฟังก์ชั่นนี้ถูกเรียกใน (เช่น) ธรรมดาคอมพิวเตอร์ 32 บิตที่มีการขัดแย้งของ 0xC000 และ 0xC000 ส่งเสริมตัวถูกดำเนินการของ*การsigned intที่จะทำให้เกิดการคำนวณให้ผลผลิต -0x10000000 ซึ่งเมื่อแปลงunsignedจะให้ผลผลิต0x90000000u--the คำตอบเช่นเดียวกับถ้าพวกเขาได้ทำส่งเสริมให้unsigned short unsignedอย่างไรก็ตามบางครั้ง gcc จะปรับฟังก์ชั่นนั้นให้เหมาะสมในลักษณะที่ไม่ทำงานหากไม่มีการล้น รหัสใด ๆ ที่มีการรวมกันของปัจจัยป้อนเข้าบางอย่างอาจทำให้เกิดการโอเวอร์โฟลว์จะต้องดำเนินการด้วย-fwrapvตัวเลือกเว้นแต่จะยอมรับได้เพื่อให้ผู้สร้างของอินพุตที่มีรูปแบบไม่ถูกต้องตั้งใจที่จะเรียกใช้รหัสโดยอำเภอใจ


1

ทำไมไม่ทำอย่างนี้:

int result = 0;
int factor = 10;
for (...) {
    factor *= 10;
    result = ...
}
return result;

แต่นั่นไม่ได้เรียกใช้...วนรอบสำหรับfactor = 1หรือfactor = 10เพียง 100 และสูงกว่า คุณต้องลอกซ้ำครั้งแรกและยังเริ่มต้นด้วยfactor = 1หากคุณต้องการให้มันทำงาน
Peter Cordes

1

มีใบหน้าที่แตกต่างกันของพฤติกรรมที่ไม่ได้กำหนดและสิ่งที่ยอมรับได้ขึ้นอยู่กับการใช้งาน

การวนรอบอย่างแน่นหนาซึ่งใช้เวลา CPU จำนวนมากในแอปพลิเคชันกราฟิกแบบเรียลไทม์

โดยตัวของมันเองเป็นสิ่งที่ผิดปกติ แต่เป็นไปตามที่มันอาจจะเป็น ... ถ้าเป็นเช่นนั้นแน่นอน UB นั้นน่าจะอยู่ในอาณาจักรมากที่สุด "ที่อนุญาตที่ยอมรับ" การเขียนโปรแกรมกราฟิกมีชื่อเสียงในเรื่องของแฮ็กและสิ่งที่น่าเกลียด ตราบใดที่มัน "ใช้งานได้" และใช้เวลาไม่เกิน 16.6 มิลลิวินาทีในการสร้างเฟรมโดยปกติจะไม่มีใครใส่ใจ แต่ถึงกระนั้นจงระวังสิ่งที่มีความหมายในการเรียกใช้ UB

ครั้งแรกมีมาตรฐาน จากมุมมองนั้นไม่มีอะไรจะพูดคุยและไม่มีทางที่จะพิสูจน์ได้ว่ารหัสของคุณไม่ถูกต้อง ไม่มี ifs หรือ whens มันไม่ใช่รหัสที่ถูกต้อง คุณอาจบอกว่าเป็นนิ้วกลางขึ้นจากมุมมองของคุณและ 95-99% ของเวลาที่คุณจะดีต่อไป

ถัดไปมีด้านฮาร์ดแวร์ มีสถาปัตยกรรมแปลก ๆ แปลก ๆที่นี่เป็นปัญหา ฉันกำลังพูดว่า"ผิดปกติแปลก"เพราะในสถาปัตยกรรมเดียวที่สร้างขึ้น 80% ของคอมพิวเตอร์ทั้งหมด (หรือทั้งสองสถาปัตยกรรมที่รวมกันทำขึ้น 95% ของคอมพิวเตอร์ทั้งหมด) ล้นเป็น"ใช่อะไรไม่สนใจ"สิ่งที่อยู่ในระดับฮาร์ดแวร์ คุณแน่ใจว่าจะได้รับขยะ (แม้ว่าจะยังคงคาดเดาได้) ผล แต่ไม่มีสิ่งชั่วร้ายเกิดขึ้น
นั่นคือไม่ใช่ในทุกกรณีสถาปัตยกรรมคุณอาจจะติดกับดักมากเกินไป (แม้ว่าจะเห็นว่าคุณพูดถึงแอพพลิเคชั่นกราฟิกอย่างไรโอกาสที่จะอยู่ในสถาปัตย์แบบแปลก ๆ นั้นค่อนข้างเล็ก) การพกพาเป็นปัญหาหรือไม่? ถ้าเป็นเช่นนั้นคุณอาจต้องการละเว้น

สุดท้ายมีด้านคอมไพเลอร์ / เพิ่มประสิทธิภาพ เหตุผลหนึ่งที่ทำให้โอเวอร์โฟลว์ไม่ได้ถูกกำหนดคือการปล่อยให้มันง่ายที่สุดที่จะรับมือกับฮาร์ดแวร์กาลครั้งหนึ่ง แต่อีกเหตุผลคือนั่นx+1คือรับประกันที่มักจะมีขนาดใหญ่กว่าxและคอมไพเลอร์ / เพิ่มประสิทธิภาพสามารถใช้ประโยชน์ความรู้นี้ ตอนนี้สำหรับกรณีที่กล่าวถึงแล้วคอมไพเลอร์เป็นที่รู้จักกันดีในการทำเช่นนี้และถอดบล็อกที่สมบูรณ์ออก (มีการใช้ประโยชน์จาก Linux เมื่อหลายปีมาแล้วซึ่งมีพื้นฐานมาจากคอมไพเลอร์
สำหรับกรณีของคุณฉันจะสงสัยอย่างจริงจังว่าคอมไพเลอร์ทำบางอย่างที่พิเศษแปลกเพิ่มประสิทธิภาพ อย่างไรก็ตามคุณรู้อะไรฉันรู้อะไร เมื่อมีข้อสงสัยลองใช้ดู. ถ้ามันใช้ได้คุณก็พร้อมที่จะไป

(และสุดท้ายมีการตรวจสอบรหัสแน่นอนคุณอาจต้องเสียเวลาพูดคุยกับผู้สอบบัญชีหากคุณโชคไม่ดี)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.