ทำไมการคำนวณทางคณิตศาสตร์มากเกินไปจึงถูกเพิกเฉย?


76

เคยลองสรุปตัวเลขทั้งหมดตั้งแต่ 1 ถึง 2,000,000 ในภาษาการเขียนโปรแกรมที่คุณชื่นชอบ? ผลลัพธ์ที่ได้นั้นง่ายต่อการคำนวณด้วยตนเอง: 2,000,001,000,000 ซึ่งมีขนาดใหญ่กว่า 900 เท่าของค่าสูงสุดของจำนวนเต็ม 32 บิตที่ไม่ได้ลงนาม

C # พิมพ์ออกมา-1453759936- ค่าลบ! และฉันเดาว่า Java ทำเช่นเดียวกัน

นั่นหมายความว่ามีบางภาษาโปรแกรมทั่วไปที่ละเว้น Arithmetic Overflow โดยค่าเริ่มต้น (ใน C # มีตัวเลือกที่ซ่อนอยู่สำหรับการเปลี่ยนแปลงนั้น) นั่นเป็นพฤติกรรมที่ดูเสี่ยงมากสำหรับฉันและไม่ใช่ความผิดพลาดของ Ariane 5 ที่เกิดจากการไหลล้นเช่นนี้ใช่ไหม

ดังนั้น: อะไรคือการตัดสินใจในการออกแบบที่อยู่เบื้องหลังพฤติกรรมที่อันตราย

แก้ไข:

คำตอบแรกสำหรับคำถามนี้แสดงค่าใช้จ่ายในการตรวจสอบที่มากเกินไป มารันโปรแกรม C # สั้น ๆ เพื่อทดสอบสมมติฐานนี้:

Stopwatch watch = Stopwatch.StartNew();
checked
{
    for (int i = 0; i < 200000; i++)
    {
        int sum = 0;
        for (int j = 1; j < 50000; j++)
        {
            sum += j;
        }
    }
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);

บนเครื่องของฉันรุ่นที่ตรวจสอบใช้เวลา 11015 มิลลิวินาทีในขณะที่รุ่นที่ไม่ได้ตรวจสอบใช้เวลา 4125 มิลลิวินาที นั่นคือขั้นตอนการตรวจสอบใช้เวลาเกือบสองเท่าในการเพิ่มตัวเลข (รวมทั้งหมด 3 เท่าของเวลาเดิม) แต่ด้วยการทำซ้ำ 10,000,000,000 ครั้งเวลาที่ใช้ในการตรวจสอบยังคงน้อยกว่า 1 นาโนวินาที อาจมีสถานการณ์ที่สำคัญ แต่สำหรับแอปพลิเคชันส่วนใหญ่นั้นจะไม่สำคัญ

แก้ไข 2:

ฉันคอมไพล์แอปพลิเคชั่นเซิร์ฟเวอร์ของเราอีกครั้ง (บริการ Windows วิเคราะห์ข้อมูลที่ได้รับจากเซ็นเซอร์หลายตัวและมีจำนวนที่เกี่ยวข้องกับ/p:CheckForOverflowUnderflow="false"พารามิเตอร์) (โดยปกติแล้วฉันจะสลับการตรวจสอบโอเวอร์โฟลว์) และปรับใช้บนอุปกรณ์ การตรวจสอบ Nagios แสดงให้เห็นว่าภาระ CPU เฉลี่ยอยู่ที่ 17%

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


19
เช่นเดียวกับบันทึกย่อสำหรับ C # คุณสามารถใช้checked { }ส่วนเพื่อทำเครื่องหมายส่วนต่าง ๆ ของรหัสที่ควรทำการตรวจสอบทางคณิตศาสตร์มากเกินไป นี่คือเนื่องจากประสิทธิภาพ
PawełŁukasik

14
"เคยพยายามที่จะสรุปตัวเลขทั้งหมดจาก 1 ถึง 2,000,000 ในภาษาการเขียนโปรแกรมที่คุณชื่นชอบ?" - (1..2_000_000).sum #=> 2000001000000ใช่: อีกภาษาที่ฉันชอบ: sum [1 .. 2000000] --=> 2000001000000. Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000ไม่ได้ชื่นชอบ: (เพื่อความยุติธรรมคนสุดท้ายคือการโกง)
Jörg W Mittag

27
@BernhardHiller Integerใน Haskell มีความแม่นยำตามอำเภอใจมันจะเก็บหมายเลขใด ๆ ไว้ตราบใดที่คุณไม่ได้ใช้ RAM ที่จัดสรรไม่ได้
Polygnome

50
ความผิดพลาดของ Ariane 5 เกิดจากการตรวจสอบการล้นที่ไม่สำคัญ - จรวดเป็นส่วนหนึ่งของเที่ยวบินที่ผลการคำนวณไม่จำเป็นต้องเพิ่มอีก แต่กลับถูกตรวจพบการไหลล้นและนั่นทำให้เที่ยวบินยกเลิก
Simon B

9
But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.นั่นเป็นข้อบ่งชี้ของการวนซ้ำที่ถูกปรับให้เหมาะสม อีกทั้งประโยคนั้นขัดแย้งกับตัวเลขก่อนหน้าซึ่งดูเหมือนจะใช้ได้กับฉันมาก
usr

คำตอบ:


86

มี 3 เหตุผลสำหรับสิ่งนี้:

  1. ค่าใช้จ่ายในการตรวจสอบโอเวอร์โฟลว์ (สำหรับการคำนวณทางคณิตศาสตร์ทุกครั้ง) ที่เวลาทำงานมากเกินไป

  2. ความซับซ้อนของการพิสูจน์ว่าสามารถข้ามการตรวจสอบมากเกินไป ณ เวลารวบรวมได้มากเกินไป

  3. ในบางกรณี (เช่นการคำนวณ CRC, ไลบรารีจำนวนมาก, ฯลฯ ) "wrap on overflow" จะสะดวกกว่าสำหรับโปรแกรมเมอร์


10
@DmitryGrigoryev unsigned intไม่ควรคำนึงถึงเพราะภาษาที่มีการตรวจสอบล้นควรจะตรวจสอบประเภทจำนวนเต็มทั้งหมดโดยค่าเริ่มต้น wrapping unsigned intคุณควรจะต้องเขียน
user253751

32
ฉันไม่ซื้ออาร์กิวเมนต์ราคา CPU จะตรวจสอบโอเวอร์โฟลว์ในการคำนวณจำนวนเต็มทุก ๆ ครั้งและตั้งค่าสถานะพกพาใน ALU มันคือการสนับสนุนภาษาการเขียนโปรแกรมที่ขาดหายไป didOverflow()ฟังก์ชั่นอินไลน์ที่เรียบง่ายหรือแม้กระทั่งตัวแปรทั่วโลก__carryที่อนุญาตให้เข้าถึงแฟล็กพกพาจะทำให้เสียเวลาเป็นศูนย์หากคุณไม่ได้ใช้งาน
slebetman

37
@slebetman: นั่นคือ x86 ARM ไม่ได้ เช่นADDไม่ได้ตั้งค่าพกพา (คุณต้องการADDS) Itanium ไม่ได้มีธงพก และแม้แต่ที่ x86, AVX ก็ยังไม่มีธง
MSalters

30
@slebetman มันตั้งค่าธงพกใช่ (บน x86, ใจคุณ) แต่คุณต้องอ่านธงพกและตัดสินใจเกี่ยวกับผลลัพธ์ - นั่นคือส่วนที่มีราคาแพง เนื่องจากการดำเนินการทางคณิตศาสตร์มักใช้ในลูป (และลูปที่แน่น) ซึ่งสามารถป้องกันการปรับให้เหมาะสมของคอมไพเลอร์ที่ปลอดภัยซึ่งสามารถส่งผลกระทบอย่างมากต่อประสิทธิภาพการทำงานแม้ว่าคุณจะต้องการคำสั่งพิเศษเพียงอย่างเดียวก็ตาม ) หมายความว่าควรเป็นค่าเริ่มต้นหรือไม่ บางทีโดยเฉพาะอย่างยิ่งในภาษาเช่น C # ซึ่งการพูดuncheckedนั้นง่ายพอ แต่คุณอาจประเมินว่ามีความสำคัญเกินความจำเป็นบ่อยเพียงใด
Luaan

12
ARM addsเป็นราคาเดียวกับadd(เป็นเพียงคำสั่งธง 1 บิตที่เลือกว่ามีการอัปเดตธงพกพาหรือไม่) addคำสั่งของ MIPS กับดักในการล้น - คุณต้องขอไม่ให้ดักกับการล้นโดยใช้adduแทน!
user253751

65

ใครบอกว่ามันเป็นการแลกเปลี่ยนที่ไม่ดี!

ฉันเรียกใช้แอพพลิเคชั่นการผลิตทั้งหมดโดยเปิดใช้งานการตรวจสอบมากเกิน นี่คือตัวเลือกคอมไพเลอร์ C # ฉันเปรียบเทียบมาตรฐานนี้และฉันไม่สามารถระบุความแตกต่างได้ ค่าใช้จ่ายในการเข้าถึงฐานข้อมูลเพื่อสร้าง (ไม่ใช่ของเล่น) HTML จะช่วยในการตรวจสอบค่าใช้จ่ายล้น

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

ในกรณีที่ฉันต้องการประสิทธิภาพซึ่งบางครั้งก็เป็นกรณีที่ฉันปิดการตรวจสอบล้นโดยใช้unchecked {}เป็นรายละเอียด เมื่อฉันต้องการโทรออกที่ฉันต้องพึ่งพาการดำเนินการที่ไม่ล้นฉันอาจเพิ่มchecked {}รหัสซ้ำลงในเอกสารจริง ฉันระวังเรื่องล้น แต่ไม่จำเป็นต้องขอบคุณการตรวจสอบ

ฉันเชื่อว่าทีม C # เลือกผิดเมื่อพวกเขาเลือกที่จะไม่ตรวจสอบการล้นโดยค่าเริ่มต้น แต่ตอนนี้ตัวเลือกนั้นถูกปิดผนึกเนื่องจากความเข้ากันได้ดี โปรดทราบว่าตัวเลือกนี้สร้างขึ้นในราวปี 2000 ฮาร์ดแวร์มีความสามารถน้อยกว่าและ. NET ยังไม่มีแรงฉุดมาก บางที. NET ต้องการที่จะดึงดูดโปรแกรมเมอร์ Java และ C / C ++ ด้วยวิธีนี้ .NET ยังหมายถึงการให้สามารถอยู่ใกล้กับโลหะ นั่นเป็นสาเหตุที่มันมีรหัสที่ไม่ปลอดภัย structs และความสามารถในการโทรภายในที่ดีซึ่ง Java ไม่มีทั้งหมด

ยิ่งฮาร์ดแวร์ของเราเร็วขึ้นและคอมไพเลอร์ที่ชาญฉลาดยิ่งขึ้นจะได้รับการตรวจสอบโอเวอร์โฟลว์ที่น่าดึงดูดยิ่งขึ้นตามค่าเริ่มต้นคือ

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

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

สำหรับบางภาษาเช่นการตรวจสอบโอเวอร์โฟลว์ C / C ++ โดยค่าเริ่มต้นนั้นไม่เหมาะสมอย่างชัดเจนเนื่องจากแอปพลิเคชันที่เขียนด้วยภาษาเหล่านี้ต้องการประสิทธิภาพของโลหะ ยังคงมีความพยายามที่จะทำให้ C / C ++ เป็นภาษาที่ปลอดภัยโดยช่วยให้การเลือกเข้าสู่โหมดความปลอดภัยมากขึ้น สิ่งนี้น่ายกย่องตั้งแต่ 90-99% ของรหัสมีแนวโน้มที่จะเย็น ตัวอย่างคือfwrapvตัวเลือกคอมไพเลอร์ที่บังคับให้มีการตัดส่วนประกอบ 2 นี่คือคุณลักษณะ "คุณภาพการปรับใช้งาน" โดยคอมไพเลอร์ไม่ใช่ภาษา

Haskell ไม่มี call call แบบลอจิคัลและไม่มีลำดับการประเมินที่ระบุ สิ่งนี้ทำให้ข้อยกเว้นเกิดขึ้นที่จุดที่ไม่สามารถคาดเดาได้ ในa + bมันไม่ได้ระบุว่ามีการประเมินaหรือไม่bก่อนและว่าการแสดงออกเหล่านั้นสิ้นสุดที่ทั้งหมดหรือไม่ ดังนั้นจึงเหมาะสมสำหรับ Haskell ที่จะใช้จำนวนเต็มที่ไม่ได้ จำกัด เวลาส่วนใหญ่ ตัวเลือกนี้เหมาะสำหรับภาษาที่ใช้งานได้จริงเพราะข้อยกเว้นนั้นไม่เหมาะสมในรหัส Haskell ส่วนใหญ่ และการหารด้วยศูนย์ย่อมเป็นปัญหาในการออกแบบภาษา Haskells แทนที่จะเป็นจำนวนเต็มที่ไม่มีขอบเขตพวกเขาสามารถใช้จำนวนเต็มความกว้างคงที่ได้ แต่ก็ไม่เหมาะกับหัวข้อ "มุ่งเน้นความถูกต้อง" ซึ่งเป็นคุณสมบัติของภาษา

ทางเลือกอื่นสำหรับข้อยกเว้นล้นคือค่าพิษที่สร้างขึ้นโดยการดำเนินการที่ไม่ได้กำหนดและเผยแพร่ผ่านการดำเนินการ (เช่นNaNค่าลอย) ที่ดูเหมือนว่าราคาแพงกว่าการตรวจสอบล้นและทำให้การดำเนินงานทั้งหมดช้าไม่ได้เป็นเพียงคนที่สามารถล้มเหลว (ยกเว้นการเร่งฮาร์ดแวร์ที่ลอยอยู่ทั่วไปมีและ ints ทั่วไปไม่ได้มี - แม้ว่าItanium มี NAT ซึ่งก็คือ "ไม่ได้เป็นสิ่ง" ) ฉันยังไม่เห็นจุดที่ทำให้โปรแกรมยังคงปวกเปียกพร้อมกับข้อมูลที่ไม่ดี ON ERROR RESUME NEXTมันก็เหมือนกับ มันซ่อนข้อผิดพลาด แต่ไม่ได้ช่วยให้ได้ผลลัพธ์ที่ถูกต้อง supercat ชี้ให้เห็นว่าบางครั้งการเพิ่มประสิทธิภาพการทำเช่นนี้


2
คำตอบที่ยอดเยี่ยม ทฤษฎีของคุณเกี่ยวกับสาเหตุที่พวกเขาตัดสินใจทำเช่นนั้น? เพียงแค่คัดลอกทุกคนที่คัดลอก C และท้ายที่สุดประกอบและไบนารี?
jpmc26

19
เมื่อ 99% ของฐานผู้ใช้ของคุณคาดว่าจะมีพฤติกรรมคุณมักจะให้มันกับพวกเขา และสำหรับ "การคัดลอก C" จริงๆแล้วมันไม่ใช่สำเนาของ C แต่เป็นส่วนขยายของมัน C รับประกันพฤติกรรมที่ปราศจากข้อยกเว้นสำหรับunsignedจำนวนเต็มเท่านั้น พฤติกรรมของล้นจำนวนเต็มลงนามเป็นจริงพฤติกรรมที่ไม่ได้กำหนดใน C และ C ++ ใช่ไม่ได้กำหนดพฤติกรรม มันเกิดขึ้นจนเกือบทุกคนนำไปใช้เป็นส่วนเกินของ 2 C # ทำให้เป็นทางการจริง ๆ แทนที่จะปล่อยให้มันเป็น UB เช่น C / C ++
Cort Ammon

10
@CortAmmon: ภาษา Dennis Ritchie ที่ออกแบบมาได้กำหนดลักษณะการทำงานแบบวิจิตรสำหรับจำนวนเต็มที่ลงนามแล้ว แต่ไม่เหมาะสำหรับการใช้งานบนแพลตฟอร์มที่ไม่ใช่ส่วนประกอบเสริม ในขณะที่การอนุญาตการเบี่ยงเบนบางอย่างจากการรวมสองอย่างที่แม่นยำสามารถช่วยเพิ่มประสิทธิภาพได้อย่างมาก (เช่นการอนุญาตให้คอมไพเลอร์เปลี่ยน x * y / y ด้วย x สามารถบันทึกการคูณและการหาร) นักเขียนคอมไพเลอร์ได้ตีความพฤติกรรมที่ไม่ได้กำหนด สิ่งที่สมเหตุสมผลสำหรับแพลตฟอร์มเป้าหมายและฟิลด์แอปพลิเคชันที่กำหนด แต่เป็นโอกาสที่จะทำให้เกิดความรู้สึกนอกหน้าต่าง
supercat

3
@CortAmmon - ตรวจสอบรหัสที่สร้างขึ้นโดยgcc -O2สำหรับx + 1 > x(ที่xเป็นint) ดูgcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/…ด้วย พฤติกรรมเสริม 2s ในการโอเวอร์โฟลว์ที่มีการลงชื่อใน C เป็นตัวเลือกแม้ในคอมไพเลอร์จริงและgccค่าเริ่มต้นที่จะเพิกเฉยในระดับการปรับให้เหมาะสมตามปกติ
Jonathan Cast

2
@supercat ใช่ผู้เขียนคอมไพเลอร์ C ส่วนใหญ่สนใจมากขึ้นในการทำให้แน่ใจว่ามาตรฐานที่ไม่สมจริงบางอย่างทำงานได้เร็วกว่า 0.5% เมื่อพยายามให้ความหมายที่สมเหตุสมผลแก่โปรแกรมเมอร์ (ใช่ฉันเข้าใจว่าทำไมมันไม่ใช่ปัญหาง่าย ๆ ในการแก้ปัญหา ผลลัพธ์ที่ไม่คาดคิดเมื่อรวมกัน, ญาดา, ญาดา แต่ก็ยังไม่ได้โฟกัสและคุณจะสังเกตเห็นถ้าคุณติดตามการสนทนา) โชคดีที่มีบางคนที่พยายามที่จะทำดีกว่า
Voo

30

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


28
ที่อย่างใดเช่นบอกว่าควรตรวจสอบการบัฟเฟอร์เกินล้นเพราะพวกเขาแทบจะไม่เคยเกิดขึ้น ...
Bernhard Hiller

73
@BernhardHiller: และนั่นคือสิ่งที่ C และ C ++ ทำ
Michael Borgwardt

12
@DavidBrown: เช่นเดียวกับโอเวอร์โฟลว์ทางคณิตศาสตร์ อดีตไม่ประนีประนอม VM แม้ว่า
Deduplicator

35
@Dupuplicator ทำให้เป็นจุดที่ยอดเยี่ยม CLR ได้รับการออกแบบอย่างระมัดระวังเพื่อให้โปรแกรมที่ตรวจสอบได้ไม่สามารถละเมิดค่าคงที่ของรันไทม์แม้ว่าจะมีสิ่งไม่ดีเกิดขึ้น แน่นอนโปรแกรมปลอดภัยสามารถละเมิดค่าคงที่ของตัวเองเมื่อสิ่งเลวร้ายเกิดขึ้น
Eric Lippert

7
การดำเนินการทางคณิตศาสตร์ @svick นั้นอาจพบได้บ่อยกว่าการดำเนินการทำดัชนีอาร์เรย์ และขนาดจำนวนเต็มส่วนใหญ่มีขนาดใหญ่พอที่จะหาเลขคณิตที่ล้นได้ยาก ดังนั้นอัตราส่วนต้นทุน - ผลประโยชน์จึงแตกต่างกันมาก
Barmar

20

อะไรคือการตัดสินใจในการออกแบบที่อยู่เบื้องหลังพฤติกรรมที่เป็นอันตราย

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

มันเป็นหนึ่งในหลักการพื้นฐานที่สุดในการออกแบบ C และ C ++ และเกิดขึ้นในช่วงเวลาที่แตกต่างกันเมื่อคุณต้องผ่านการหัวเราะที่ไร้สาระเพื่อให้ได้ประสิทธิภาพที่เพียงพอสำหรับงานที่แทบไม่ได้รับการพิจารณาในปัจจุบัน

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


18
แน่นอนว่าไม่ใช่การกำกับดูแลในการออกแบบของ C # นักออกแบบของ C # สร้างสองโหมดโดยเจตนา: checkedและuncheckedเพิ่มไวยากรณ์สำหรับการสลับระหว่างโหมดเหล่านั้นแบบโลคัลและสวิตช์บรรทัดคำสั่ง (และการตั้งค่าโครงการใน VS) เพื่อเปลี่ยนแปลงทั่วโลก คุณอาจไม่เห็นด้วยกับการuncheckedตั้งค่าเริ่มต้น (ฉันทำ) แต่ทั้งหมดนี้ชัดเจนมาก
svick

8
@slebetman - เพียงเพื่อบันทึก: ค่าใช้จ่ายที่นี่ไม่ใช่ค่าใช้จ่ายในการตรวจสอบการโอเวอร์โฟลว์ (ซึ่งไม่สำคัญ) แต่ค่าใช้จ่ายในการใช้รหัสที่แตกต่างกันขึ้นอยู่กับว่ามีการล้นเกิดขึ้น (ซึ่งแพงมาก) ซีพียูไม่ชอบคำสั่งสาขาตามเงื่อนไข
Jonathan Cast

5
@jcast การคาดคะเนสาขาในตัวประมวลผลสมัยใหม่แทบจะไม่กำจัดคำสั่งสาขาที่มีเงื่อนไข หลังจากที่ทุกกรณีปกติไม่ควรล้นดังนั้นจึงเป็นพฤติกรรมการแตกแขนงที่คาดเดาได้มาก
CodeMonkey

4
เห็นด้วยกับ @CodeMonkey คอมไพเลอร์จะใส่ในการกระโดดแบบมีเงื่อนไขในกรณีที่มีมากเกินไปไปยังหน้าที่ไม่ปกติโหลด / เย็น การคาดการณ์เริ่มต้นสำหรับสิ่งนั้นคือ "ไม่ได้รับ" และมันอาจจะไม่เปลี่ยนแปลง ค่าใช้จ่ายทั้งหมดเป็นคำสั่งเดียวในไปป์ไลน์ แต่นั่นเป็นหนึ่งค่าใช้จ่ายในการเรียนการสอนต่อการเรียนการสอนคณิตศาสตร์
MSalters

2
@MSalters ใช่มีค่าใช้จ่ายเพิ่มเติมในการเรียนการสอน และผลกระทบอาจมีขนาดใหญ่หากคุณมีปัญหาเกี่ยวกับ CPU โดยเฉพาะ ในแอปพลิเคชั่นส่วนใหญ่ที่มีการผสมผสานของรหัสหนัก IO และ CPU ฉันคาดว่าผลกระทบจะน้อยที่สุด ฉันชอบวิธีสนิมในการเพิ่มค่าใช้จ่ายเฉพาะใน Debug builds แต่ลบออกใน Release builds
CodeMonkey

20

มรดก

ฉันจะบอกว่าปัญหานี้มีรากฐานมาจากมรดก ใน C:

  • การโอเวอร์โฟลว์ที่ลงนามแล้วเป็นพฤติกรรมที่ไม่ได้กำหนด (คอมไพเลอร์สนับสนุนแฟล็กเพื่อทำให้ห่อ)
  • การโอเวอร์โฟลที่ไม่ได้ลงชื่อถูกกำหนดพฤติกรรม

สิ่งนี้ทำเพื่อให้ได้ประสิทธิภาพที่ดีที่สุดตามหลักการที่โปรแกรมเมอร์รู้ว่ามันทำอะไรอยู่

นำไปสู่ ​​Statu-Quo

ความจริงที่ว่า C (และโดยส่วนขยาย C ++) ไม่จำเป็นต้องมีการตรวจจับการไหลล้นในการเปิดหมายความว่าการตรวจสอบการล้นเกินจะชะลอตัว

ฮาร์ดแวร์ส่วนใหญ่ให้ความสำคัญกับ C / C ++ (อย่างจริงจัง x86 มีstrcmpคำสั่ง (หรือที่เรียกว่าPCMPISTRIตั้งแต่ SSE 4.2)!) และเนื่องจาก C ไม่สนใจซีพียูทั่วไปจึงไม่สามารถตรวจจับกระแสเกินได้อย่างมีประสิทธิภาพ ใน x86 คุณต้องตรวจสอบการตั้งค่าสถานะสำหรับแต่ละคอร์หลังจากการดำเนินการที่อาจเกิดการโอเวอร์โฟลว์ เมื่อสิ่งที่คุณต้องการจริง ๆ คือการตั้งค่าสถานะ "เสีย" ในผลลัพธ์ (คล้ายกับ NaN propagates) และการดำเนินการของเวคเตอร์อาจเป็นปัญหาได้มากกว่า ผู้เล่นใหม่บางรายอาจปรากฏในตลาดพร้อมการจัดการล้นอย่างมีประสิทธิภาพ แต่ตอนนี้ x86 และ ARM ไม่สนใจ

เครื่องมือเพิ่มประสิทธิภาพคอมไพเลอร์ไม่ดีในการเพิ่มประสิทธิภาพการตรวจสอบโอเวอร์โฟลว์หรือแม้กระทั่งการปรับให้เหมาะสมเมื่อมีโอเวอร์โฟลว์ นักวิชาการบางคนเช่น John Regher บ่นเกี่ยวกับ statu-quoนี้ แต่ความจริงก็คือเมื่อความจริงง่ายๆของการทำ overflows "ความล้มเหลว" นั้นป้องกันการปรับให้เหมาะสมแม้กระทั่งก่อนที่การชุมนุมจะพบกับซีพียู โดยเฉพาะอย่างยิ่งเมื่อมันป้องกัน auto-vectorization ...

พร้อมเอฟเฟกต์แบบเรียงซ้อน

ดังนั้นหากไม่มีกลยุทธ์การเพิ่มประสิทธิภาพที่มีประสิทธิภาพและการสนับสนุน CPU ที่มีประสิทธิภาพการตรวจสอบโอเวอร์โฟลว์จึงมีค่าใช้จ่ายสูง มีราคาแพงกว่าการห่อ

เพิ่มพฤติกรรมที่น่ารำคาญบางอย่างเช่นx + y - 1อาจล้นเมื่อx - 1 + yไม่ได้ซึ่งอาจรบกวนผู้ใช้อย่างถูกกฎหมายและการตรวจสอบมากเกินไปจะถูกยกเลิกโดยทั่วไปในการตัดคำ (ซึ่งจัดการตัวอย่างนี้และอื่น ๆ อย่างสง่างาม)

ถึงกระนั้นก็ยังไม่หมดหวัง

มีความพยายามในคอมไพเลอร์แบบ clang และ gcc เพื่อใช้งาน "sanitizers": วิธีในการใช้ไบนารีเครื่องมือเพื่อตรวจสอบกรณีของพฤติกรรมที่ไม่ได้กำหนด เมื่อใช้-fsanitize=undefinedงานจะตรวจพบโอเวอร์โฟลว์ที่ลงนามแล้ว มีประโยชน์มากในระหว่างการทดสอบ

สนิมการเขียนโปรแกรมภาษามีล้นตรวจสอบการเปิดใช้งานโดยค่าเริ่มต้นในโหมด Debug (มันใช้ห่อคณิตศาสตร์ในโหมดรุ่นสำหรับเหตุผลประสิทธิภาพ)

ดังนั้นจึงมีความกังวลมากขึ้นเกี่ยวกับการตรวจสอบล้นและอันตรายของผลลัพธ์ปลอมจะไม่ถูกตรวจจับและหวังว่าสิ่งนี้จะส่งผลให้เกิดความสนใจในชุมชนการวิจัยชุมชนคอมไพเลอร์และชุมชนฮาร์ดแวร์


6
@DmitryGrigoryev ที่ตรงข้ามของวิธีที่มีประสิทธิภาพในการตรวจสอบสำหรับล้นเช่นใน Haswell จะช่วยลดการส่งผ่านจาก 4 เพิ่มเติมปกติต่อรอบเพียง 1 ตรวจสอบนอกจากนี้และนั่นคือก่อนที่จะพิจารณาผลกระทบของสาขา mispredictions ของjo's และ ผลกระทบทั่วโลกของมลภาวะที่เพิ่มเข้ากับสถานะตัวพยากรณ์สาขาและขนาดรหัสที่เพิ่มขึ้น หากค่าสถานะนั้นเหนียวมันจะให้ศักยภาพที่แท้จริง .. และจากนั้นคุณก็ยังไม่สามารถทำได้ในโค้ดเวกเตอร์

3
เนื่องจากคุณกำลังเชื่อมโยงไปยังโพสต์บล็อกที่เขียนโดย John Regehr ฉันคิดว่ามันเหมาะสมที่จะเชื่อมโยงไปยังบทความอื่นของเขาเขียนสองสามเดือนก่อนหน้าที่คุณลิงก์ บทความเหล่านี้พูดถึงปรัชญาที่แตกต่าง: ในบทความก่อนหน้าจำนวนเต็มเป็นขนาดคงที่ มีการตรวจสอบเลขคณิตจำนวนเต็ม (เช่นรหัสไม่สามารถดำเนินการต่อได้); มีข้อยกเว้นหรือกับดัก บทความที่ใหม่กว่าพูดถึงการทำจำนวนเต็มขนาดคงที่พร้อมกันซึ่งจะช่วยขจัดปัญหาล้น

2
@rwong จำนวนเต็มไม่มีที่สิ้นสุดมีปัญหาเช่นกัน หากล้นของคุณเป็นผลมาจากข้อผิดพลาด (ซึ่งมักจะเป็น) มันอาจกลายเป็นความผิดพลาดอย่างรวดเร็วเป็นความเจ็บปวดนานที่ใช้ทรัพยากรเซิร์ฟเวอร์ทั้งหมดจนกว่าทุกอย่างจะล้มเหลวอย่างน่ากลัว ฉันเป็นแฟนตัวยงของวิธีการ "ล้มเหลวเร็ว" - โอกาสน้อยที่จะวางยาพิษให้กับสภาพแวดล้อมทั้งหมด ฉันต้องการประเภท Pascal-ish 1..100แทน - ให้ชัดเจนเกี่ยวกับช่วงที่คาดหวังแทนที่จะเป็น "บังคับ" เป็น 2 ^ 31 ฯลฯ บางภาษาเสนอเรื่องนี้แน่นอนและพวกเขามักจะทำการตรวจสอบมากเกินไปตามค่าเริ่มต้น รวบรวมเวลาแม้)
Luaan

1
@ Luaan: สิ่งที่น่าสนใจคือบ่อยครั้งที่การคำนวณระดับกลางอาจล้นชั่วคราว แต่ผลลัพธ์ไม่ได้ ตัวอย่างเช่นในช่วง 1..100 ของคุณx * 2 - 2อาจล้นเมื่อxถึง 51 แม้ว่าผลลัพธ์จะพอดีบังคับให้คุณจัดเรียงลำดับการคำนวณของคุณใหม่ (บางครั้งเป็นไปในทางที่ผิดธรรมชาติ) จากประสบการณ์ของฉันฉันพบว่าโดยทั่วไปแล้วฉันชอบที่จะทำการคำนวณในรูปแบบที่ใหญ่กว่านั้นจากนั้นตรวจสอบว่าผลลัพธ์นั้นเหมาะสมหรือไม่
Matthieu M.

1
@MatthieuM ใช่นั่นคือสิ่งที่คุณเข้าสู่ดินแดน "คอมไพเลอร์ที่ฉลาดพอ" ตามหลักการแล้วค่า 103 ควรใช้ได้สำหรับประเภท 1..100 ตราบใดที่ไม่เคยใช้ในบริบทที่คาดว่ามีค่าจริง 1..100 (เช่นx = x * 2 - 2ควรทำงานกับทุกสิ่งxที่การมอบหมายนั้นให้ผลลัพธ์ที่ถูกต้อง 1 .100 จำนวน) นั่นคือการดำเนินการกับชนิดตัวเลขอาจมีความแม่นยำสูงกว่าชนิดของตัวเองตราบใดที่การกำหนดเหมาะสม สิ่งนี้จะมีประโยชน์มากในกรณี(a + b) / 2ที่การละเลยการล้น (ไม่ได้ลงชื่อ) อาจเป็นตัวเลือกที่ถูกต้อง
Luaan

10

ภาษาที่พยายามตรวจจับโอเวอร์โฟลว์ได้กำหนดความหมายในอดีตที่เกี่ยวข้องในรูปแบบที่ จำกัด อย่างรุนแรงว่าอะไรจะเป็นประโยชน์สูงสุด เหนือสิ่งอื่นใดในขณะที่มักจะมีประโยชน์ในการคำนวณในลำดับที่แตกต่างจากสิ่งที่ระบุในรหัสภาษาส่วนใหญ่ที่มีการดักข้อมูลมากเกินจะรับประกันว่ารหัสที่ได้รับเช่น:

for (int i=0; i<100; i++)
{
  Operation1();
  x+=i;
  Operation2();
}

หากค่าเริ่มต้นของ x จะทำให้เกิดโอเวอร์โฟลว์ในการผ่าน 47 ในลูป Operation1 จะดำเนินการ 47 ครั้งและ Operation2 จะดำเนินการ 46 ในกรณีที่ไม่มีการรับประกันดังกล่าวหากไม่มีสิ่งอื่นใดในลูปใช้ x และไม่มีอะไรเลย จะใช้ค่า x ตามข้อยกเว้นที่ส่งออกมาโดย Operation1 หรือ Operation2 รหัสอาจถูกแทนที่ด้วย:

x+=4950;
for (int i=0; i<100; i++)
{
  Operation1();
  Operation2();
}

น่าเสียดายที่การดำเนินการปรับให้เหมาะสมดังกล่าวในขณะที่รับประกันความหมายที่ถูกต้องในกรณีที่เกิดโอเวอร์โฟลว์ภายในลูปนั้นเป็นเรื่องยาก

if (x < INT_MAX-4950)
{
  x+=4950;
  for (int i=0; i<100; i++)
  {
    Operation1();
    Operation2();
  }
}
else
{
  for (int i=0; i<100; i++)
  {
    Operation1();
    x+=i;
    Operation2();
  }
}

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

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

(*) หากภาษาสัญญาว่าจะมีการรายงานการโอเวอร์โฟลว์ทั้งหมดการแสดงออกเช่นx*y/yนั้นไม่สามารถทำให้ง่ายขึ้นxเว้นแต่ว่าx*yจะรับประกันได้ว่าจะไม่ล้น ในทำนองเดียวกันแม้ว่าผลลัพธ์ของการคำนวณจะถูกละเว้นภาษาที่สัญญาว่าจะรายงานการโอเวอร์โฟลว์ทั้งหมดจะต้องดำเนินการต่อไปเพื่อให้สามารถทำการตรวจสอบโอเวอร์โฟลว์ได้ เนื่องจากการโอเวอร์โฟลว์ในกรณีดังกล่าวไม่สามารถส่งผลให้เกิดพฤติกรรมที่ไม่ถูกต้องทางคณิตศาสตร์โปรแกรมไม่จำเป็นต้องทำการตรวจสอบดังกล่าวเพื่อรับประกันว่าไม่มีการล้นเกินที่ทำให้เกิดผลลัพธ์ที่ไม่ถูกต้อง

อนึ่งการไหลล้นใน C นั้นแย่มากโดยเฉพาะ แม้ว่าแพลตฟอร์มฮาร์ดแวร์เกือบทุกตัวที่รองรับ C99 จะใช้ซีแมนทิกส์แบบปิดเสียงสองส่วนเสริม แต่ก็มีความทันสมัยสำหรับคอมไพเลอร์สมัยใหม่ในการสร้างรหัสซึ่งอาจทำให้เกิดผลข้างเคียงโดยพลการในกรณีที่มีการล้น ตัวอย่างเช่นให้บางสิ่งเช่น:

#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
  uint32_t total=0;
  q|=32768;
  for (int i = 32768; i<=q; i++)
  {
    total+=test(i,65535);
    *p+=1;
  }
  return total;
}

GCC จะสร้างรหัสสำหรับ test2 ซึ่งเพิ่มขึ้นอย่างไม่มีเงื่อนไข (* p) หนึ่งครั้งและส่งกลับ 32768 โดยไม่คำนึงถึงค่าที่ส่งผ่านไปยัง q จากการให้เหตุผลการคำนวณ (32769 * 65535) & 65535u จะทำให้เกิดการล้นและไม่จำเป็นต้องให้คอมไพเลอร์พิจารณากรณีใด ๆ ที่ (q | 32768) จะให้ค่าที่มากกว่า 32768 แม้ว่าจะไม่มี เหตุผลที่การคำนวณ (32769 * 65535) & 65535u ควรสนใจบิตส่วนบนของผลลัพธ์ gcc จะใช้การโอเวอร์โฟลว์ที่มีการเซ็นชื่อเป็นเหตุผลในการละเว้นการวนรอบ


2
"มันทันสมัยสำหรับคอมไพเลอร์สมัยใหม่ ... " - ในทำนองเดียวกันมันเป็นแฟชั่นสั้น ๆ สำหรับนักพัฒนาของเมล็ดที่รู้จักกันดีบางอย่างเพื่อเลือกที่จะไม่อ่านเอกสารที่เกี่ยวข้องกับการใช้ธงปรับให้เหมาะสมที่พวกเขาใช้แล้วโกรธทั่วอินเทอร์เน็ต เพราะพวกเขาถูกบังคับให้เพิ่มธงคอมไพเลอร์มากขึ้นเพื่อให้ได้พฤติกรรมที่ต้องการ ;-) ในกรณีนี้-fwrapvส่งผลให้เกิดพฤติกรรมที่กำหนดแม้ว่าจะไม่ใช่พฤติกรรมที่ผู้ถามต้องการ ได้รับการเพิ่มประสิทธิภาพ gcc ทำให้การพัฒนา C ทุกชนิดเป็นการสอบอย่างละเอียดเกี่ยวกับมาตรฐานและพฤติกรรมของคอมไพเลอร์
Steve Jessop

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

1
... หากโปรแกรมเมอร์ต้องการประเมินx+y > zในแบบที่ไม่เคยทำอะไรนอกจากผลผลิต 0 หรือผลตอบแทน 1 แต่ผลลัพธ์ใด ๆ อาจเป็นที่ยอมรับเท่ากันในกรณีที่โอเวอร์โฟลเลอร์คอมไพเลอร์ที่เสนอการรับประกันนั้นสามารถสร้างรหัสที่ดีกว่าสำหรับ การแสดงออกx+y > zกว่าคอมไพเลอร์ใด ๆ จะสามารถสร้างสำหรับการแสดงออกรุ่น defensively แนบเนียนพูดสิ่งที่ส่วนของที่มีประโยชน์เพิ่มประสิทธิภาพล้นที่เกี่ยวข้องจะได้รับการจรรยาบรรณโดยการรับประกันว่าการคำนวณจำนวนเต็มอื่นที่ไม่ใช่ส่วน / ส่วนที่เหลือจะดำเนินการโดยไม่มีผลข้างเคียงหรือไม่?
supercat

ฉันสารภาพว่าฉันไม่ได้ลงรายละเอียดอย่างเต็มที่ แต่ความจริงที่ว่าคุณไม่พอใจกับ "นักเขียนคอมไพเลอร์" โดยทั่วไปและไม่เฉพาะเจาะจง "ใครบางคนใน gcc ที่ไม่ยอมรับ-fwhatever-makes-senseแพทช์ของฉัน" แนะนำให้ฉันอย่างยิ่งว่า มันมากกว่าอย่างรวดเร็วในส่วนของพวกเขา ข้อโต้แย้งตามปกติที่ฉันได้ยินคือโค้ดอินไลน์ (และแม้กระทั่งการขยายมาโคร) ได้รับประโยชน์จากการลดขนาดให้มากที่สุดเท่าที่จะเป็นไปได้เกี่ยวกับการใช้โค้ดที่สร้างขึ้นโดยเฉพาะเนื่องจากสิ่งใดก็ตาม เพื่อที่รหัสรอบ "พิสูจน์" เป็นไปไม่ได้
Steve Jessop

ดังนั้นสำหรับตัวอย่างที่ง่ายขึ้นถ้าฉันเขียนfoo(i + INT_MAX + 1)คอมไพเลอร์ - นักเขียนมีความกระตือรือร้นที่จะใช้การปรับให้เหมาะสมกับfoo()โค้ดอินไลน์ซึ่งต้องอาศัยความถูกต้องในการโต้แย้งว่าไม่ใช่เชิงลบ (เทคนิค divmod โหดเหี้ยมอาจ) ภายใต้ข้อ จำกัด เพิ่มเติมของคุณพวกเขาสามารถใช้การเพิ่มประสิทธิภาพที่พฤติกรรมการป้อนข้อมูลเชิงลบเหมาะสมสำหรับแพลตฟอร์มเท่านั้น แน่นอนว่าโดยส่วนตัวฉันยินดีที่จะเป็น-fตัวเลือกที่เปิดและ-fwrapvอื่น ๆ และน่าจะต้องปิดการใช้งานการเพิ่มประสิทธิภาพบางอย่างที่ไม่มีการตั้งค่าสถานะ แต่มันก็ไม่เหมือนกับว่าฉันสามารถใส่ใจที่จะทำทุกอย่างที่ทำงานด้วยตัวเอง
Steve Jessop

9

ภาษาการเขียนโปรแกรมบางภาษาไม่สนใจการโอเวอร์โฟลว์จำนวนเต็ม บางภาษามีการดำเนินการจำนวนเต็มที่ปลอดภัยสำหรับตัวเลขทั้งหมด (ภาษา Lisp ส่วนใหญ่, Ruby, Smalltalk, ... ) และอื่น ๆ ผ่านทางห้องสมุด - เช่นมี BigInt คลาสต่างๆสำหรับ C ++

ไม่ว่าภาษาจะทำให้จำนวนเต็มปลอดภัยจากการล้นโดยค่าเริ่มต้นหรือไม่ขึ้นอยู่กับวัตถุประสงค์: ภาษาของระบบเช่น C และ C ++ จำเป็นต้องจัดเตรียมบทคัดย่อค่าใช้จ่ายเป็นศูนย์และ "เลขจำนวนเต็มขนาดใหญ่" ไม่ใช่หนึ่งเดียว ภาษาที่ใช้ในการเพิ่มประสิทธิภาพเช่น Ruby สามารถและจัดทำเลขจำนวนเต็มขนาดใหญ่นอกกรอบได้ ภาษาเช่น Java และ C # ที่อยู่ระหว่างควร IMHO ไปกับจำนวนเต็มปลอดภัยออกจากกล่องโดยพวกเขาไม่


โปรดทราบว่ามีความแตกต่างระหว่างการตรวจจับการโอเวอร์โฟลว์ (และจากนั้นมีสัญญาณ, ความตื่นตระหนก, ข้อยกเว้น, ... ) และการสลับไปเป็นจำนวนมาก อดีตควรจะเป็นไปได้มากราคาถูกกว่าหลัง
Matthieu M.

@MatthieuM อย่างแน่นอน - และฉันรู้ว่าฉันไม่ชัดเจนเกี่ยวกับเรื่องนั้นในคำตอบของฉัน
Nemanja Trifunovic

7

ดังที่คุณได้แสดง C # จะช้าลง 3 เท่าหากเปิดใช้งานการตรวจสอบมากเกินไปโดยค่าเริ่มต้น (สมมติว่าตัวอย่างของคุณเป็นแอปพลิเคชันทั่วไปสำหรับภาษานั้น) ฉันยอมรับว่าประสิทธิภาพไม่ได้เป็นคุณสมบัติที่สำคัญที่สุดเสมอไป แต่โดยทั่วไปภาษา / คอมไพเลอร์จะถูกนำมาเปรียบเทียบกับประสิทธิภาพในงานทั่วไป นี่คือส่วนหนึ่งเนื่องจากความจริงที่ว่าคุณภาพของคุณสมบัติภาษาค่อนข้างอัตนัยในขณะที่การทดสอบประสิทธิภาพเป็นวัตถุประสงค์

หากคุณแนะนำภาษาใหม่ซึ่งคล้ายกับ C # ในแง่มุมส่วนใหญ่ แต่ช้ากว่า 3 เท่าการได้รับส่วนแบ่งตลาดจะไม่ใช่เรื่องง่ายแม้ว่าในท้ายที่สุดผู้ใช้ปลายทางส่วนใหญ่ของคุณจะได้รับประโยชน์จากการตรวจสอบมากเกิน จากประสิทธิภาพที่สูงขึ้น


10
นี่เป็นกรณีพิเศษสำหรับ C # ซึ่งอยู่ในช่วงแรก ๆ เมื่อเทียบกับ Java และ C ++ ที่ไม่ใช่ตัวชี้วัดประสิทธิภาพการทำงานของนักพัฒนาซึ่งยากที่จะวัดได้หรือบนตัวชี้วัดการประหยัดเงินสดจากการไม่ติดต่อกับการรักษาความปลอดภัย ซึ่งยากที่จะวัดได้ แต่ใช้การวัดประสิทธิภาพเล็กน้อย
Eric Lippert

1
และน่าจะเป็นไปได้ว่าประสิทธิภาพของ CPU นั้นได้รับการตรวจสอบด้วยการใช้ตัวเลขที่เรียบง่าย ดังนั้นการปรับให้เหมาะสมสำหรับการตรวจจับน้ำล้นอาจให้ผลลัพธ์ที่ "แย่" ในการทดสอบเหล่านั้น จับ 22.
Bernhard Hiller

5

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

  1. การคำนวณการจัดทำดัชนี (การทำดัชนีอาร์เรย์และ / หรือเลขคณิตของตัวชี้)

  2. เลขคณิตอื่น ๆ

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

ดังนั้นการตรวจสอบการจัดสรรหน่วยความจำก็เพียงพอแล้วเมื่อทำงานกับตัวชี้เลขคณิตและนิพจน์การทำดัชนีที่เกี่ยวข้องกับโครงสร้างข้อมูลที่จัดสรร ตัวอย่างเช่นหากคุณมีพื้นที่ที่อยู่แบบ 32 บิตและใช้จำนวนเต็มแบบ 32 บิตและอนุญาตให้ฮีปจัดสรรได้สูงสุด 2GB (ประมาณครึ่งหนึ่งของพื้นที่ที่อยู่) การคำนวณดัชนี / ตัวชี้ (โดยทั่วไป) จะไม่ล้น

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

การคำนวณที่ไม่ใช่การทำดัชนีและไม่ใช่ตัวชี้ทั้งหมดควรจัดเป็นประเภทที่ต้องการ / คาดหวังการล้น (เช่นการคำนวณการแปลงแป้นพิมพ์) และรายการที่ไม่ได้ (เช่นตัวอย่างการรวมของคุณ)

ในกรณีหลังโปรแกรมเมอร์มักจะใช้ชนิดข้อมูลทางเลือกเช่นหรือบางส่วนdouble BigIntการคำนวณจำนวนมากต้องการdecimalประเภทข้อมูลมากกว่าdoubleเช่นการคำนวณทางการเงิน หากพวกเขาทำไม่ได้และติดกับประเภทจำนวนเต็มพวกเขาจะต้องระมัดระวังในการตรวจสอบจำนวนเต็มล้น - หรืออื่น ๆ ใช่โปรแกรมสามารถไปถึงเงื่อนไขข้อผิดพลาดตรวจไม่พบในขณะที่คุณกำลังชี้ให้เห็น

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


3

ภาษาRustจัดเตรียมการประนีประนอมที่น่าสนใจระหว่างการตรวจสอบโอเวอร์โฟลว์และไม่ใช่โดยการเพิ่มการตรวจสอบสำหรับบิลด์การดีบักและการลบออกในเวอร์ชันรีลีสที่ดีที่สุด สิ่งนี้ช่วยให้คุณค้นหาข้อผิดพลาดระหว่างการทดสอบในขณะที่ยังคงประสิทธิภาพการทำงานเต็มรูปแบบในรุ่นสุดท้าย

เนื่องจากบางครั้งการห่อหุ้มล้นเป็นพฤติกรรมที่ต้องการบางครั้งก็มีเวอร์ชั่นของโอเปอเรเตอร์ที่ไม่เคยตรวจสอบโอเวอร์โฟลว์

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับเหตุผลที่อยู่เบื้องหลังตัวเลือกในRFCสำหรับการเปลี่ยนแปลง นอกจากนี้ยังมีข้อมูลที่น่าสนใจมากมายในโพสต์บล็อกนี้รวมถึงรายการข้อบกพร่องที่คุณสมบัตินี้ช่วยในการจับ


2
สนิมยังมีวิธีการต่าง ๆ เช่นchecked_mulนี้เพื่อตรวจสอบว่ามีการล้นเกิดขึ้นและส่งกลับNoneถ้าเป็นเช่นนั้นSomeหรือไม่ สามารถใช้ในการผลิตเช่นเดียวกับโหมดการแก้ปัญหา: doc.rust-lang.org/std/primitive.i32.html#examples-15
Akavall

3

ใน Swift จะมีการตรวจพบโอเวอร์โฟลว์จำนวนเต็มใด ๆ โดยค่าเริ่มต้นและหยุดโปรแกรมทันที ในกรณีที่คุณต้องการการทำงานแบบวิจิตรมีตัวดำเนินการ & +, & - และ & * ที่แตกต่างกัน และมีฟังก์ชั่นที่ดำเนินการและบอกว่ามีการล้นหรือไม่

มันสนุกมากที่จะได้เห็นผู้เริ่มต้นลองประเมินลำดับของ Collatz และมีรหัสของพวกมันพัง :-)

ตอนนี้นักออกแบบของ Swift ก็เป็นนักออกแบบของ LLVM และ Clang ดังนั้นพวกเขาจึงรู้เพียงเล็กน้อยหรือสองเรื่องเกี่ยวกับการปรับให้เหมาะสมและมีความสามารถในการหลีกเลี่ยงการตรวจสอบที่ไม่จำเป็น เมื่อเปิดใช้งานการเพิ่มประสิทธิภาพทั้งหมดการตรวจสอบล้นจะไม่เพิ่มขนาดรหัสและเวลาดำเนินการมากนัก และเนื่องจากโอเวอร์โฟลว์ส่วนใหญ่นำไปสู่ผลลัพธ์ที่ไม่ถูกต้องอย่างแท้จริงจึงเป็นขนาดรหัสและเวลาในการดำเนินการที่ใช้ไปอย่างดีเยี่ยม

PS ใน C, C ++, โอเวอร์โฟลว์ทางคณิตศาสตร์จำนวนเต็มที่ลงนามใน Objective-C เป็นพฤติกรรมที่ไม่ได้กำหนด นั่นหมายความว่าสิ่งที่คอมไพเลอร์ทำในกรณีของจำนวนเต็มล้นลงนามถูกต้องตามนิยาม วิธีทั่วไปในการจัดการกับจำนวนเต็มล้นที่มีการลงนามคือการเพิกเฉยไม่ว่าผลอะไรที่ CPU ให้คุณจะสร้างสมมติฐานในคอมไพเลอร์ว่าการล้นดังกล่าวจะไม่เกิดขึ้น (และสรุปว่า n + 1> n เป็นจริงเสมอเนื่องจากล้นคือ สันนิษฐานว่าจะไม่เกิดขึ้น) และความเป็นไปได้ที่ไม่ค่อยมีใครใช้คือการตรวจสอบและพังหากมีการล้นเกิดขึ้นเช่นเดียวกับ Swift


1
บางครั้งฉันก็สงสัยว่าคนที่ผลักดันความบ้าคลั่งของ UB ใน C นั้นพยายามแอบบ่อนทำลายภาษาอื่นบ้างไหม นั่นจะทำให้รู้สึก
supercat

การx+1>xถือว่าเป็นความจริงแบบไม่มีเงื่อนไขจะไม่ต้องการคอมไพเลอร์ในการสร้าง "สมมติฐาน" ใด ๆ เกี่ยวกับ x หากคอมไพเลอร์ได้รับอนุญาตให้ประเมินนิพจน์จำนวนเต็มโดยใช้ประเภทที่มีขนาดใหญ่ตามอำเภอใจโดยสะดวก (หรือประพฤติราวกับว่าทำ ตัวอย่างที่น่าสนใจของ "สมมติฐาน" ที่มากเกินไปจะเป็นการตัดสินใจว่าการให้uint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }คอมไพเลอร์สามารถใช้sum += mul(65535, x)ในการตัดสินใจว่าxไม่ควรเกิน 32768 [พฤติกรรมที่น่าจะทำให้ตกใจคนที่เขียนเหตุผล C89 ซึ่งแสดงให้เห็นว่าหนึ่งในปัจจัยการตัดสินใจ ..
supercat

... ในการunsigned shortส่งเสริมให้signed intเป็นความจริงที่ว่า two's-เติมเต็มการใช้งานเงียบวิจิตร (เช่นส่วนใหญ่ของการใช้งาน C แล้วในการใช้งาน) จะรักษารหัสเหมือนข้างต้นทางเดียวกันว่าunsigned shortการเลื่อนตำแหน่งให้หรือint unsignedมาตรฐานไม่จำเป็นต้องมีการใช้งานบนฮาร์ดแวร์เสริมสองตัวที่ทำงานเงียบ ๆ เพื่อรักษารหัสเช่นเดียวกับข้างต้นอย่างมีเหตุผล แต่ผู้เขียนมาตรฐานดูเหมือนจะคาดหวังว่าพวกเขาจะทำเช่นนั้น
supercat

2

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

เหตุผลที่ใช้งานได้คือการเป็นตัวแทนที่สมบูรณ์ของ 2 ของจำนวนเต็มที่ลงนามซึ่งซีพียูทุกตัวใช้ ตัวอย่างเช่นในส่วนเสริม 4-bit 2 การเพิ่ม 5 และ -3 มีลักษณะดังนี้:

  0101   (5)
  1101   (-3)
(11010)  (carry)
  ----
  0010   (2)

สังเกตว่าพฤติกรรมการล้อมรอบของการทิ้งบิตที่นำออกมานั้นให้ผลที่ได้รับการลงนามที่ถูกต้องอย่างไร ในทำนองเดียวกันซีพียูมักจะดำเนินการลบx - yเป็นx + ~y + 1:

  0101   (5)
  1100   (~3, binary negation!)
(11011)  (carry, we carry in a 1 bit!)
  ----
  0010   (2)

การดำเนินการลบนี้เป็นการเพิ่มเติมในฮาร์ดแวร์ปรับแต่งอินพุตเฉพาะกับ arithmetical-logical-unit (ALU) ในรูปแบบที่ไม่สำคัญ อะไรที่ง่ายกว่านี้?

เนื่องจากการคูณนั้นไม่มีอะไรอื่นนอกจากลำดับของการเพิ่มเติมมันจึงทำงานในลักษณะที่คล้ายกัน ผลลัพธ์ของการใช้การแทนค่าส่วนเสริม 2 และการละเว้นการดำเนินการทางคณิตศาสตร์นั้นทำให้วงจรง่ายขึ้นและชุดคำสั่งที่ง่ายขึ้น

เห็นได้ชัดว่าตั้งแต่ C ถูกออกแบบมาให้ทำงานใกล้กับโลหะมันจึงใช้พฤติกรรมเช่นนี้เหมือนกับพฤติกรรมมาตรฐานของเลขคณิตที่ไม่ได้ลงนาม และตัวเลือกนั้นถูกนำไปใช้กับภาษาอื่นเช่น Java และแน่นอนว่า C #


ฉันมาที่นี่เพื่อให้คำตอบนี้เช่นกัน
นาย Lister

น่าเสียดายที่บางคนคิดว่าไม่มีเหตุผลอย่างไม่มีเหตุผลที่คนเขียนโค้ด C ระดับต่ำบนแพลตฟอร์มควรมีความกล้าที่จะคาดหวังว่าคอมไพเลอร์ C ที่เหมาะสมกับวัตถุประสงค์ดังกล่าวจะมีพฤติกรรมที่ จำกัด ในกรณีที่มีการล้น ส่วนตัวผมคิดว่ามันเหมาะสมสำหรับคอมไพเลอร์จะทำตัวราวกับว่าการคำนวณจะดำเนินการโดยใช้ความแม่นยำโดยพลการขยายความสะดวกของคอมไพเลอร์ของ (ดังนั้นในระบบ 32 บิตถ้าx==INT_MAXแล้วx+1โดยพลการอาจทำงานเป็นทั้ง 2147483648 หรือ -2147483648 ที่คอมไพเลอร์ของ ความสะดวกสบาย) แต่ ...
supercat

บางคนดูเหมือนจะคิดว่าถ้าxและyมีuint16_tและรหัสบนระบบ 32 บิตคำนวณx*y & 65535uเมื่อyเป็น 65535 คอมไพเลอร์ควรคิดรหัสที่จะไม่ถึงเมื่อxมีค่ามากกว่า 32768.
SuperCat

1

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

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

แต่ด้วยการทำซ้ำ 10,000,000,000 ครั้งเวลาที่ใช้ในการตรวจสอบยังคงน้อยกว่า 1 นาโนวินาที

มีบางสิ่งผิดปกติในการใช้เหตุผลนี้:

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

  2. 1 นาโนวินาทีอาจดูเหมือนไม่มีอะไรสำหรับส่วนของรหัสที่ทำงานไม่บ่อยนัก ในทางตรงกันข้ามถ้ามันอยู่ในวงด้านในของการคำนวณบางอย่างที่เป็นหน้าที่หลักของรหัสดังนั้นทุก ๆ ส่วนของเวลาที่คุณสามารถโกนออกจะทำให้เกิดความแตกต่างใหญ่ หากคุณใช้การจำลองในคลัสเตอร์แล้วเศษส่วนที่บันทึกไว้ของ nanosecond ในวงในของคุณสามารถแปลเป็นเงินที่ใช้กับฮาร์ดแวร์และไฟฟ้าได้โดยตรง

  3. สำหรับอัลกอริทึมและบริบทบางอย่างการทำซ้ำ 10,000,000,000 ครั้งอาจไม่มีนัยสำคัญ อีกครั้งโดยทั่วไปไม่เหมาะสมที่จะพูดคุยเกี่ยวกับสถานการณ์เฉพาะที่ใช้ในบริบทบางอย่างเท่านั้น

อาจมีสถานการณ์ที่สำคัญ แต่สำหรับแอปพลิเคชันส่วนใหญ่นั้นจะไม่สำคัญ

บางทีคุณพูดถูก แต่นี่เป็นเรื่องของเป้าหมายของภาษานั้น ๆ ในความเป็นจริงหลายภาษาได้รับการออกแบบมาเพื่อรองรับความต้องการของ "ส่วนใหญ่" หรือเพื่อความปลอดภัยมากกว่าความกังวลอื่น ๆ อื่น ๆ เช่น C และ C ++ ให้ความสำคัญกับประสิทธิภาพ ในบริบทนั้นทำให้ทุกคนจ่ายค่าปรับประสิทธิภาพเพียงเพราะคนส่วนใหญ่ไม่ต้องกังวลไปกับภาษาที่พยายามจะทำให้สำเร็จ


-1

มีคำตอบที่ดี แต่ฉันคิดว่ามีจุดพลาดที่นี่: ผลกระทบของจำนวนเต็มล้นไม่จำเป็นต้องเป็นสิ่งที่ไม่ดีและจากความจริงมันยากที่จะรู้ว่าiเป็นMAX_INTไปMIN_INTได้หรือไม่เนื่องจากปัญหาล้น หรือถ้ามันตั้งใจทำโดยการคูณด้วย -1

ตัวอย่างเช่นหากฉันต้องการเพิ่มจำนวนเต็มที่แทนได้ทั้งหมดมากกว่า 0 เข้าด้วยกันฉันจะใช้การfor(i=0;i>=0;++i){...}เพิ่มลูป - และเมื่อมันล้นก็หยุดการเพิ่มซึ่งเป็นพฤติกรรมเป้าหมาย (การโยนข้อผิดพลาดจะหมายความว่าฉันต้องหลีกเลี่ยง การป้องกันตามอำเภอใจเพราะมันรบกวนเลขคณิตมาตรฐาน) มันเป็นการปฏิบัติที่ไม่ถูกต้องในการ จำกัด เลขคณิตดั้งเดิมเนื่องจาก

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

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

1
คุณไม่สามารถไปINT_MAXถึงINT_MINด้วยการคูณด้วย -1
David Conrad

เห็นได้ชัดว่าการแก้ปัญหาคือการให้วิธีการสำหรับโปรแกรมเมอร์ที่จะปิดการตรวจสอบในบล็อกของรหัสหรือหน่วยการรวบรวมที่กำหนด
David Conrad

for(i=0;i>=0;++i){...}เป็นรูปแบบของรหัสที่ฉันพยายามกีดกันในทีมของฉัน: ขึ้นอยู่กับเอฟเฟกต์พิเศษ / ผลข้างเคียงและไม่ได้แสดงอย่างชัดเจนว่าควรจะทำยังไง แต่ฉันก็ยังคงชื่นชมคำตอบของคุณเพราะมันแสดงกระบวนทัศน์การเขียนโปรแกรมที่แตกต่างกัน
Bernhard Hiller

1
@Delioth: หากiเป็นประเภท 64 บิตแม้ในการใช้งานที่มีการทำงานที่สมบูรณ์แบบของการทำงานสองพันล้านครั้งต่อวินาทีการวนซ้ำนั้นสามารถรับประกันได้ว่าจะหาintค่าที่ใหญ่ที่สุดหากอนุญาตให้เรียกใช้ หลายร้อยปี บนระบบที่ไม่รับประกันว่าจะมีพฤติกรรมการปิดล้อมแบบเงียบ ๆ พฤติกรรมดังกล่าวจะไม่สามารถรับประกันได้ไม่ว่าจะได้รับรหัสนานเท่าใด
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.