การเพิ่มประสิทธิภาพจุดลอยตัวนี้อนุญาตหรือไม่


90

ฉันพยายามตรวจสอบว่าที่ไหนfloatสูญเสียความสามารถในการแทนจำนวนเต็มจำนวนมาก ฉันจึงเขียนตัวอย่างเล็ก ๆ น้อย ๆ นี้:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

ดูเหมือนว่าโค้ดนี้จะใช้ได้กับคอมไพเลอร์ทั้งหมดยกเว้นเสียงดัง เสียงดังสร้างการวนซ้ำที่ไม่สิ้นสุดที่เรียบง่าย Godbolt .

อนุญาตหรือไม่ ถ้าใช่มันเป็นปัญหา QoI หรือไม่?


@geza ฉันสนใจที่จะได้ยินจำนวนผลลัพธ์!
ดา

5
gccทำการเพิ่มประสิทธิภาพลูปแบบไม่มีที่สิ้นสุดเหมือนกันหากคุณคอมไพล์-Ofastแทนดังนั้นการเพิ่มประสิทธิภาพจึงgccถือว่าไม่ปลอดภัย แต่ก็สามารถทำได้
12345ieee

3
g ++ ยังสร้างลูปที่ไม่มีที่สิ้นสุด แต่ไม่ได้เพิ่มประสิทธิภาพการทำงานจากภายใน คุณสามารถดูได้ucomiss xmm0,xmm0เพื่อเปรียบเทียบ(float)iกับตัวมันเอง นั่นเป็นเบาะแสแรกของคุณว่าแหล่งที่มา C ++ ของคุณไม่ได้หมายความว่าคุณคิดว่ามันทำอะไร คุณอ้างว่าคุณมีลูปนี้เพื่อพิมพ์ / ส่งคืน16777216หรือไม่? คอมไพเลอร์ / เวอร์ชัน / อ็อพชันนั้นมีอะไรบ้าง? เพราะนั่นจะเป็นบั๊กของคอมไพเลอร์ gcc ปรับรหัสของคุณให้เหมาะสมjnpเป็นสาขาลูปอย่างถูกต้อง( godbolt.org/z/XJYWeu ): ให้วนลูปต่อไปตราบเท่าที่ตัวถูกดำเนินการ!= ไม่ใช่ NaN
Peter Cordes

4
โดยเฉพาะอย่างยิ่งเป็น-ffast-mathตัวเลือกที่เปิดใช้งานโดยปริยาย-Ofastซึ่งอนุญาตให้ GCC ใช้การเพิ่มประสิทธิภาพจุดลอยตัวที่ไม่ปลอดภัยและสร้างรหัสเดียวกันกับเสียงดัง MSVC ทำงานในลักษณะเดียวกันทุกประการ: หากไม่มี/fp:fastก็จะสร้างรหัสจำนวนมากที่ส่งผลให้เกิดการวนซ้ำแบบไม่สิ้นสุด ด้วย/fp:fastมันจะส่งเสียงjmpคำสั่งเดียว ฉันสมมติว่าหากไม่มีการเปิดใช้การเพิ่มประสิทธิภาพ FP ที่ไม่ปลอดภัยอย่างชัดเจนคอมไพเลอร์เหล่านี้จะติดอยู่กับข้อกำหนด IEEE 754 เกี่ยวกับค่า NaN ค่อนข้างน่าสนใจที่ Clang ไม่ได้จริงๆ เครื่องวิเคราะห์แบบคงที่ดีกว่า @ 12345ieee
โคดี้เกรย์

1
@geza: หากโค้ดทำตามที่คุณต้องการให้ตรวจสอบว่าเมื่อใดที่ค่าทางคณิตศาสตร์(float) iแตกต่างจากค่าทางคณิตศาสตร์ของiผลลัพธ์ (ค่าที่ส่งคืนในreturnคำสั่ง) จะเป็น 16,777,217 ไม่ใช่ 16,777,216
Eric Postpischil

คำตอบ:


49

ดังที่ @Angew ชี้ให้เห็นตัว!=ดำเนินการต้องการแบบเดียวกันทั้งสองด้าน (float)i != iผลลัพธ์ในโปรโมชั่นของ RHS (float)i != (float)iจะลอยเป็นอย่างดีเพื่อให้เรามี


g ++ ยังสร้างลูปที่ไม่มีที่สิ้นสุด แต่ไม่ได้เพิ่มประสิทธิภาพการทำงานจากภายใน คุณสามารถเห็นมันแปลง int-> ลอยด้วยcvtsi2ssและucomiss xmm0,xmm0เปรียบเทียบ(float)iกับตัวมันเอง (นั่นเป็นเบาะแสแรกของคุณที่แหล่งที่มา C ++ ของคุณไม่ได้หมายความว่าสิ่งที่คุณคิดไว้เหมือนที่คำตอบของ @ Angew อธิบาย)

x != xจะเป็นจริงก็ต่อเมื่อ "ไม่เรียงลำดับ" เพราะxเป็น NaN ( INFINITYเปรียบเทียบกับตัวมันเองในคณิตศาสตร์ IEEE แต่ NaN ไม่ NAN == NANเป็นเท็จNAN != NANเป็นจริง)

gcc7.4 และเก่ากว่าปรับแต่งโค้ดของคุณให้เหมาะสมjnpเป็นสาขาลูป ( https://godbolt.org/z/fyOhW1 ): วนลูปต่อไปตราบเท่าที่ตัวถูกดำเนินการx != x ไม่ใช่ NaN (gcc8 และในภายหลังยังตรวจสอบjeเพื่อแยกออกจากลูปไม่สามารถปรับให้เหมาะสมได้โดยอิงจากข้อเท็จจริงที่ว่ามันจะเป็นจริงเสมอสำหรับอินพุตที่ไม่ใช่ NaN) x86 FP เปรียบเทียบชุด PF กับ unordered


และ BTW นั่นหมายความว่าการเพิ่มประสิทธิภาพของ clang นั้นปลอดภัยเช่นกันโดยจะต้องมี CSE (float)i != (implicit conversion to float)iเหมือนกันและพิสูจน์ได้ว่าi -> floatไม่มี NaN สำหรับช่วงที่เป็นไปได้ของint.

(แม้ว่าลูปนี้จะเข้าสู่ UB ที่มีการลงชื่อมากเกินไป แต่ก็อนุญาตให้ปล่อย asm ที่ต้องการได้อย่างแท้จริงรวมถึงud2คำสั่งที่ผิดกฎหมายหรือการวนซ้ำที่ว่างเปล่าโดยไม่คำนึงว่าเนื้อหาของลูปเป็นอย่างไร) แต่การเพิกเฉยต่อ UB ที่ลงนามมากเกินไป การเพิ่มประสิทธิภาพนี้ยังคงถูกกฎหมาย 100%


GCC ล้มเหลวในการเพิ่มประสิทธิภาพของร่างกายออกไปห่วงแม้จะมีการ-fwrapvที่จะทำให้การลงนามจำนวนเต็มล้นที่ดีที่กำหนด (เป็น 2 ส่วนเติมเต็มวิจิตร) https://godbolt.org/z/t9A8t_

แม้แต่การเปิดใช้งาน-fno-trapping-mathก็ไม่ช่วย (ค่าเริ่มต้นของ GCC น่าเสียดายที่จะเปิดใช้งาน
-ftrapping-mathแม้ว่าการใช้งาน GCC จะเสีย / มีข้อผิดพลาด) การแปลง int-> float อาจทำให้เกิดข้อยกเว้นที่ไม่แน่นอนของ FP (สำหรับตัวเลขที่ใหญ่เกินไปที่จะแสดงอย่างแน่นอน) ดังนั้นด้วยข้อยกเว้นที่อาจเปิดเผยได้จึงมีเหตุผลที่จะไม่ เพิ่มประสิทธิภาพร่างกายห่วง (เนื่องจากการแปลง16777217เป็น float อาจมีผลข้างเคียงที่สังเกตเห็นได้หากไม่มีการปิดบังข้อยกเว้นที่ไม่แน่นอน)

แต่ด้วย-O3 -fwrapv -fno-trapping-mathการเพิ่มประสิทธิภาพที่พลาดไป 100% ที่จะไม่รวบรวมสิ่งนี้เป็นลูปที่ไม่มีที่สิ้นสุด หากไม่มี#pragma STDC FENV_ACCESS ONสถานะของแฟล็กติดหนึบที่บันทึกข้อยกเว้นของ FP ที่ถูกปิดบังจะไม่ใช่ผลข้างเคียงที่สังเกตได้ของโค้ด ไม่int-> floatการแปลงอาจทำให้เกิด NaN ดังนั้นจึงx != xไม่สามารถเป็นจริงได้


คอมไพเลอร์เหล่านี้มีการเพิ่มประสิทธิภาพทั้งหมดสำหรับ C ++ การใช้งานที่ใช้ IEEE 754 ความแม่นยำเดียว (binary32) floatและ int32

การแก้ปัญหา(int)(float)i != iห่วงจะมี UB ใน c ++ การใช้งานกับแคบ 16 บิตintและ / หรือที่กว้างขึ้นfloatเพราะคุณจะตีลงนามจำนวนเต็มล้น UB floatก่อนที่จะถึงจำนวนเต็มแรกที่ไม่ตรงซึ่งแสดงเป็น

แต่ UB ภายใต้ชุดตัวเลือกที่กำหนดการนำไปใช้งานที่แตกต่างกันจะไม่มีผลเสียใด ๆ เมื่อคอมไพล์สำหรับการนำไปใช้งานเช่น gcc หรือ clang ด้วย x86-64 System V ABI


BTW คุณสามารถคำนวณผลลัพธ์ของลูปนี้แบบคงที่จากFLT_RADIXและที่FLT_MANT_DIGกำหนดใน<climits>. หรืออย่างน้อยคุณก็สามารถในทางทฤษฎีถ้าfloatจริง ๆ แล้วเหมาะกับแบบจำลองของการลอยตัวของ IEEE มากกว่าการแทนจำนวนจริงประเภทอื่น ๆ เช่น Posit / unum

ฉันไม่แน่ใจว่ามาตรฐาน ISO C ++ เกี่ยวกับfloatพฤติกรรมมากน้อยเพียงใดและรูปแบบที่ไม่ได้ขึ้นอยู่กับฟิลด์เลขชี้กำลังและนัยสำคัญคงที่จะเป็นไปตามมาตรฐานหรือไม่


ในความคิดเห็น:

@geza ฉันสนใจที่จะได้ยินจำนวนผลลัพธ์!

@nada: มัน 16777216

คุณอ้างว่าคุณมีลูปนี้เพื่อพิมพ์ / ส่งคืน16777216หรือไม่?

อัปเดต: เนื่องจากความคิดเห็นนั้นถูกลบฉันคิดว่าไม่ อาจเป็นไปได้ว่า OP เป็นเพียงการอ้างถึงfloatจำนวนเต็มแรกที่ไม่สามารถแสดงเป็น 32 บิตfloatได้ทั้งหมด https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values นั่นคือสิ่งที่พวกเขาหวังจะตรวจสอบด้วยรหัสบั๊กกี้นี้

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

(ค่าลอยตัวที่สูงกว่าทั้งหมดเป็นจำนวนเต็มแน่นอน แต่เป็นจำนวนทวีคูณของ 2 จากนั้น 4 แล้ว 8 เป็นต้นสำหรับค่าเลขชี้กำลังที่สูงกว่าความกว้างนัยสำคัญสามารถแสดงค่าจำนวนเต็มที่สูงกว่าจำนวนมากได้ แต่จะมี 1 หน่วยในตำแหน่งสุดท้าย (ของนัยสำคัญ) มีค่ามากกว่า 1 ดังนั้นจึงไม่เป็นจำนวนเต็มติดกันจำนวน จำกัด ที่ใหญ่ที่สุดfloatคือต่ำกว่า 2 ^ 128 ซึ่งใหญ่เกินไปสำหรับคู่int64_t)

หากคอมไพเลอร์ตัวใดออกจากลูปเดิมและพิมพ์สิ่งนั้นจะเป็นบั๊กของคอมไพเลอร์


3
@SombreroChicken: ไม่ฉันเรียนอิเล็กทรอนิกส์ก่อน (จากตำราบางเล่มที่พ่อของฉันนอนอยู่รอบ ๆ เขาเป็นศาสตราจารย์ฟิสิกส์) จากนั้นตรรกะดิจิทัลและเข้าสู่ซีพียู / ซอฟต์แวร์หลังจากนั้น : P ค่อนข้างมากฉันชอบทำความเข้าใจสิ่งต่างๆตั้งแต่ต้นหรือถ้าฉันเริ่มต้นด้วยระดับที่สูงขึ้นฉันก็ชอบที่จะเรียนรู้อย่างน้อยบางอย่างเกี่ยวกับระดับที่ต่ำกว่าซึ่งมีผลต่อวิธีการ / ทำไมสิ่งต่างๆจึงทำงานได้ดีในระดับที่ฉัน คิดเกี่ยวกับ. (เช่นวิธีการทำงานของ asm และวิธีการปรับให้เหมาะสมนั้นได้รับอิทธิพลจากข้อ จำกัด ในการออกแบบ CPU / สิ่งที่สถาปัตยกรรม cpu ซึ่งจะมาจากฟิสิกส์ + คณิตศาสตร์)
Peter Cordes

1
GCC อาจไม่สามารถปรับให้เหมาะสมได้frapwแต่ฉันแน่ใจว่า GCC 10 -ffinite-loopsออกแบบมาสำหรับสถานการณ์เช่นนี้
MCCCS

64

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

(float)i != (float)i

สิ่งนี้ไม่ควรล้มเหลวและในที่สุดโค้ดก็จะล้นออกiมาทำให้โปรแกรมของคุณไม่ได้กำหนดพฤติกรรม พฤติกรรมใด ๆ จึงเป็นไปได้

ในการตรวจสอบสิ่งที่คุณต้องการตรวจสอบอย่างถูกต้องคุณควรส่งผลลัพธ์กลับไปที่int:

if ((int)(float)i != i)

8
@ Džurisมันคือ UB มีคือไม่มีผลที่ชัดเจนอย่างใดอย่างหนึ่ง คอมไพเลอร์อาจรู้ว่ามันสามารถจบลงใน UB และตัดสินใจที่จะลบลูปทั้งหมด
คดีของ Fund Monica

4
@opa คุณหมายถึงstatic_cast<int>(static_cast<float>(i))? reinterpret_castเห็นได้ชัดว่า UB มี
Caleth

6
@NicHartley: คุณบอกว่า(int)(float)i != iเป็น UB? คุณสรุปได้อย่างไร? ใช่มันขึ้นอยู่กับคุณสมบัติที่กำหนดในการนำไปใช้งาน (เนื่องจากfloatไม่จำเป็นต้องเป็น IEEE754 binary32) แต่สำหรับการนำไปใช้งานใด ๆ จะมีการกำหนดไว้อย่างดีเว้นแต่จะfloatสามารถแทนintค่าที่เป็นบวกทั้งหมดได้ทั้งหมดดังนั้นเราจึงได้รับ UB ล้นจำนวนเต็มที่ลงนาม ( en.cppreference.com/w/cpp/types/climitsกำหนดFLT_RADIXและFLT_MANT_DIGกำหนดสิ่งนี้) โดยทั่วไปสิ่งที่กำหนดการใช้งานการพิมพ์เช่นstd::cout << sizeof(int)ไม่ใช่ UB ...
Peter Cordes

2
@Caleth: reinterpret_cast<int>(float)ไม่ใช่ UB อย่างแน่นอนมันเป็นเพียงข้อผิดพลาดทางไวยากรณ์ / รูปแบบไม่ถูกต้อง คงจะดีถ้าไวยากรณ์นั้นอนุญาตให้ใช้ type-punning of float intเป็นทางเลือกอื่นmemcpy(ซึ่งมีการกำหนดไว้อย่างดี) แต่reinterpret_cast<>ฉันคิดว่าใช้ได้กับประเภทตัวชี้เท่านั้น
Peter Cordes

2
@Peter Just for NaN x != xเป็นเรื่องจริง ดูสดบน coliru ใน C ด้วย.
Deduplicator
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.