ถ้าฉันคัดลอกทุ่นไปยังตัวแปรอื่นพวกมันจะเท่ากันหรือไม่?


167

ฉันรู้ว่าการใช้==เพื่อตรวจสอบความเท่าเทียมกันของตัวแปร floating-point นั้นไม่ใช่วิธีที่ดี แต่ฉันแค่อยากรู้ว่าด้วยคำสั่งต่อไปนี้:

float x = ...

float y = x;

assert(y == x)

ตั้งแต่yคัดลอกมาจากxการยืนยันจะเป็นจริงหรือไม่


78
ผมขอมอบ 50 เงินให้กับคนที่พิสูจน์ความไม่เท่าเทียมกันโดยการสาธิตด้วยรหัสจริง ฉันต้องการที่จะเห็นสิ่งที่ 80 vs 64 บิตในการดำเนินการ บวกอีก 50 สำหรับคำอธิบายของรหัสแอสเซมเบลอร์ที่สร้างขึ้นซึ่งแสดงให้เห็นตัวแปรหนึ่งที่อยู่ในการลงทะเบียนและอื่น ๆ ที่ไม่ใช่ (หรือสาเหตุของความไม่เท่าเทียมใด ๆ ก็ตาม
Thomas Weller

1
@ThomasWeller ข้อบกพร่อง GCC เกี่ยวกับสิ่งนี้: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; อย่างไรก็ตามฉันเพิ่งพยายามทำซ้ำในระบบ x86-64 และมันไม่ได้แม้แต่ -fast-math ฉันสงสัยว่าคุณต้องการ GCC แบบเก่าในระบบ 32 บิต
pjc50

5
@ pjc50: จริงๆแล้วคุณจำเป็นต้องมีระบบ 80 บิตในการทำซ้ำข้อผิดพลาด 323; มันคือ 80x87 FPU ที่ทำให้เกิดปัญหา x86-64 ใช้ SSE FPU บิตพิเศษที่ทำให้เกิดปัญหาเพราะพวกเขาถูกปัดเศษเมื่อหกค่าไปลอย 32 บิต
MSalters

4
หากทฤษฎีของ MSalters นั้นถูกต้อง (และฉันสงสัยว่ามันเป็น) คุณสามารถทำซ้ำได้โดยการรวบรวม 32- บิต ( -m32) หรือโดยการสั่งให้ GCC ใช้ x87 FPU ( -mfpmath=387)
Cody Gray

4
เปลี่ยน "48 bit" เป็น "80 bit" จากนั้นคุณสามารถลบคำคุณศัพท์ "mythical" ที่นั่น @Hot นั่นคือสิ่งที่ถูกพูดคุยทันทีก่อนที่ความคิดเห็นของคุณ x87 (FPU สำหรับสถาปัตยกรรม x86) ใช้การลงทะเบียนแบบ 80 บิตซึ่งเป็นรูปแบบ "ความแม่นยำแบบขยาย"
Cody Gray

คำตอบ:


125

นอกจากassert(NaN==NaN);กรณีที่ชี้ให้เห็นโดย kmdreko คุณสามารถมีสถานการณ์กับ x87- คณิตศาสตร์เมื่อ 80 บิตลอยถูกเก็บไว้ในหน่วยความจำชั่วคราวและในภายหลังเมื่อเทียบกับค่าที่ยังคงเก็บอยู่ในทะเบียน

ตัวอย่างที่เป็นไปได้น้อยที่สุดซึ่งล้มเหลวด้วย gcc9.2 เมื่อรวบรวมด้วย-O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

การสาธิต Godbolt: https://godbolt.org/z/X-Xt4R

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

ดูการอ้างอิงคำถามที่พบบ่อยของ GCC:


2
ดูเหมือนว่าแปลกที่บิตพิเศษจะได้รับการพิจารณาเมื่อเปรียบเทียบfloatกับความแม่นยำมาตรฐานกับความแม่นยำพิเศษ
Nat

13
@Nat มันเป็นแปลก; นี่เป็นข้อผิดพลาด
การแข่งขัน Lightness ใน Orbit

13
@ThomasWeller ไม่นั่นเป็นรางวัลที่สมเหตุสมผล แม้ว่าฉันต้องการคำตอบเพื่อชี้ให้เห็นว่านี่เป็นพฤติกรรมที่ไม่เข้ากัน
การแข่งขัน Lightness ใน Orbit

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

4
มูลค่าการกล่าวขวัญที่-ffloat-storeดูเหมือนจะเป็นวิธีการป้องกันนี้
OrangeDog

116

มันจะไม่เป็นจริงถ้าxเป็นNaNเพราะการเปรียบเทียบNaNนั้นเป็นเท็จเสมอ (ใช่แม้กระทั่งNaN == NaN) สำหรับกรณีอื่นทั้งหมด (ค่าปกติค่า subnormal, infinities, ศูนย์) การยืนยันนี้จะเป็นจริง

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


การประเมินแบบขยายที่แม่นยำควรเป็นแบบที่ไม่มีปัญหาหากปฏิบัติตามมาตรฐาน จากการ<cfloat>สืบทอดจาก C [5.2.4.2.2.8] ( เหมืองที่เน้น ):

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

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


10
เกิดอะไรขึ้นถ้าxคำนวณในการลงทะเบียนในบรรทัดแรก, floatการรักษาความแม่นยำมากขึ้นกว่าขั้นต่ำสำหรับ y = xอาจจะอยู่ในหน่วยความจำการรักษาเฉพาะfloatที่มีความแม่นยำ จากนั้นการทดสอบความเท่าเทียมกันจะทำกับหน่วยความจำกับทะเบียนที่มีความแตกต่างกันจึงไม่มีการรับประกัน
David Schwartz

5
x+pow(b,2)==x+pow(a,3)อาจแตกต่างจากauto one=x+pow(b,2); auto two=y+pow(a,3); one==twoเพราะหนึ่งอาจเปรียบเทียบโดยใช้ความแม่นยำมากกว่าอื่น ๆ (ถ้าหนึ่ง / สองเป็นค่า 64 บิตใน ram ในขณะที่ค่า intermediste เป็น 80ish บิตบน fpu) ดังนั้นการมอบหมายสามารถทำบางสิ่งบางอย่าง
Yakk - Adam Nevraumont

22
@evg แน่นอน! คำตอบของฉันเป็นไปตามมาตรฐาน การเดิมพันทั้งหมดจะปิดถ้าคุณบอกว่าคอมไพเลอร์ของคุณไม่สับสนโดยเฉพาะเมื่อเปิดใช้งานการคำนวณอย่างรวดเร็ว
kmdreko

11
@Voo ดูคำพูดในคำตอบของฉัน ค่าของ RHS ถูกกำหนดให้กับตัวแปรบน LHS ไม่มีเหตุผลทางกฎหมายสำหรับค่าผลลัพธ์ของ LHS ที่จะแตกต่างจากค่าของ RHS ฉันขอขอบคุณที่คอมไพเลอร์หลายคนมีข้อบกพร่องในเรื่องนี้ แต่สิ่งที่เก็บไว้ในการลงทะเบียนควรจะไม่มีอะไรเกี่ยวข้องกับมัน
การแข่งขัน Lightness ใน Orbit

6
@Voo: ใน ISO C ++ การปัดเศษเป็นความกว้างของประเภทจะเกิดขึ้นกับการกำหนดใด ๆ ในคอมไพเลอร์ส่วนใหญ่ที่กำหนดเป้าหมาย x87 จะเกิดขึ้นเมื่อคอมไพเลอร์ตัดสินใจที่จะหก / โหลดซ้ำ คุณสามารถบังคับgcc -ffloat-storeให้ปฏิบัติตามอย่างเคร่งครัด แต่คำถามนี้เกี่ยวกับการx=y; x==y; ไม่ทำอะไรเลยทั้งในและระหว่าง หากyมีการปัดเศษให้พอดีกับการลอยการแปลงเป็น double หรือ long double และ back จะไม่เปลี่ยนค่า ...
Peter Cordes

34

ใช่yจะใช้มูลค่าของx:

[expr.ass]/2: ในการมอบหมายอย่างง่าย (=) วัตถุที่ถูกอ้างถึงโดยตัวถูกดำเนินการด้านซ้ายถูกแก้ไข ([defns.access]) โดยการแทนที่ค่าด้วยผลลัพธ์ของตัวถูกดำเนินการด้านขวา

ไม่มีค่าใช้จ่ายเพิ่มเติมสำหรับการกำหนดค่าอื่น ๆ

(คนอื่น ๆ ได้ชี้ให้เห็นแล้วว่าการเปรียบเทียบความเท่าเทียมกัน==จะยังคงประเมินfalseสำหรับค่า NaN)

ปัญหาปกติของ floating-point ==ก็คือมันไม่ง่ายเลยที่จะมีค่าเท่าที่คุณคิด ที่นี่เรารู้ว่าค่าสองค่าไม่ว่าจะเป็นอะไรก็ตาม


7
@ThomasWeller นั่นเป็นข้อผิดพลาดที่ทราบกันดีในการใช้งานที่ไม่สอดคล้อง ดีที่จะพูดถึงมัน!
การแข่งขัน Lightness ใน Orbit

ในตอนแรกฉันคิดว่าภาษาที่ใช้กฎหมายแยกความแตกต่างระหว่าง "คุณค่า" และ "ผลลัพธ์" จะเป็นสิ่งผิดเพี้ยน แต่ความแตกต่างนี้ไม่จำเป็นต้องไม่มีความแตกต่างโดยภาษา C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6 หรือ C5.3, 7.1.6 ของร่างมาตรฐานที่คุณอ้างถึง
Eric Towers

@EricTowers ขออภัยคุณสามารถอธิบายการอ้างอิงเหล่านั้นได้หรือไม่ ฉันไม่พบสิ่งที่คุณกำลังชี้ไป
การแข่งขัน Lightness ใน Orbit

@ LightnessRacesBY-SA3.0: C C2.2 , C3.3 , C4.2และC5.3
Eric Towers

@EricTowers ใช่แล้วยังไม่ติดตามคุณ ลิงค์แรกของคุณไปที่ดัชนีภาคผนวก C (ไม่บอกอะไรเลย) [expr]สี่การเชื่อมโยงต่อไปของคุณไปตลอดไป หากฉันไม่สนใจลิงก์และมุ่งเน้นไปที่การอ้างอิงฉันจะสับสนกับคำกล่าวที่ว่าC.5.3ดูเหมือนจะไม่ใช้คำว่า "คุณค่า" หรือคำว่า "ผล" (แม้ว่าจะเป็นเช่นนั้น ใช้ "ผลลัพธ์" หนึ่งครั้งในบริบทภาษาอังกฤษปกติ) บางทีคุณอาจอธิบายได้อย่างชัดเจนมากขึ้นว่าคุณคิดว่ามาตรฐานสร้างความแตกต่างได้อย่างไรและให้การอ้างอิงที่ชัดเจนถึงเหตุการณ์นี้ ขอบคุณ!
การแข่งขัน Lightness ใน Orbit

3

ใช่ในทุกกรณี (ไม่สนใจปัญหาของ NaNs และ x87) สิ่งนี้จะเป็นจริง

หากคุณทำmemcmpกับพวกเขาคุณจะสามารถทดสอบความเท่าเทียมกันในขณะที่สามารถเปรียบเทียบ NaNs และ sNaNs สิ่งนี้จะต้องการคอมไพเลอร์เพื่อใช้ที่อยู่ของตัวแปรซึ่งจะบังคับค่าเป็น 32 บิตfloatแทนที่จะเป็น 80 บิต สิ่งนี้จะกำจัดปัญหา x87 การยืนยันครั้งที่สองที่นี่มีวัตถุประสงค์เพื่อล้มเหลวในการแสดงว่า==จะไม่เปรียบเทียบ NaNs เป็นจริง:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

โปรดทราบว่าถ้า NaN มีการเป็นตัวแทนภายในที่แตกต่างกัน (เช่น mantissa ที่แตกต่างกัน) memcmpจะไม่เปรียบเทียบความจริง


1

ในกรณีปกติมันจะประเมินเป็นจริง (หรือข้อความยืนยันจะไม่ทำอะไรเลย)

แก้ไข :

โดย 'กรณีปกติ' ฉันหมายถึงฉันไม่รวมสถานการณ์จำลองข้างต้น (เช่นค่า NaN และหน่วยจุดลอยตัว 80x87) ตามที่ผู้ใช้รายอื่นชี้

ด้วยความล้าสมัยของชิป 8087 ในบริบทปัจจุบันปัญหานี้ค่อนข้างจะแยกกันและสำหรับคำถามที่จะใช้ในสถานะปัจจุบันของสถาปัตยกรรมจุดลอยตัวที่ใช้จริงในทุกกรณียกเว้น NaNs

(อ้างอิงเกี่ยวกับ 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

รุ่งโรจน์ถึง @chtz เพื่อทำซ้ำตัวอย่างที่ดีและ @kmdreko เพื่อพูดถึง NaNs - ไม่เคยรู้มาก่อนเลย!


1
ฉันคิดว่ามันเป็นไปได้ทั้งหมดxที่จะอยู่ในการลงทะเบียนจุดลอยในขณะที่yโหลดจากหน่วยความจำ หน่วยความจำอาจมีความแม่นยำน้อยกว่าการลงทะเบียนทำให้การเปรียบเทียบล้มเหลว
David Schwartz

1
นั่นอาจเป็นกรณีของปลอมฉันไม่ได้คิดอย่างนั้น (เนื่องจาก OP ไม่มีกรณีพิเศษใด ๆ ฉันถือว่าไม่มีข้อ จำกัด เพิ่มเติม)
Anirban166

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

6
การแก้ไขทำให้คำตอบนี้ไม่ถูกต้อง มาตรฐาน C ++ กำหนดให้การแปลงค่าเป็นประเภทปลายทาง - ความแม่นยำส่วนเกินอาจใช้ในการประเมินผลนิพจน์ แต่อาจไม่ถูกเก็บไว้ผ่านการกำหนด มันไม่มีสาระสำคัญว่าค่าจะถูกเก็บไว้ในทะเบียนหรือหน่วยความจำ มาตรฐาน C ++ นั้นจำเป็นต้องมีตามที่เขียนไว้เป็นfloatค่าที่ไม่มีความแม่นยำเป็นพิเศษ
Eric Postpischil

2
@AProgrammer เนื่องจากว่านักสะสมบั๊กกี้ (มีจำนวนมาก) อาจทำให้เกิดint a=1; int b=a; assert( a==b );การยืนยันได้ในทางทฤษฎีฉันคิดว่ามันสมเหตุสมผลที่จะตอบคำถามนี้เกี่ยวกับคอมไพเลอร์ที่ทำงานได้อย่างถูกต้อง (ในขณะที่อาจสังเกตว่าคอมไพเลอร์บางรุ่น - รู้จักกันดีว่าให้ทำสิ่งนี้ผิด) ในแง่การปฏิบัติหากด้วยเหตุผลบางอย่างคอมไพเลอร์ไม่ได้ลบความแม่นยำพิเศษจากผลของการมอบหมายที่เก็บไว้ลงทะเบียนก็ควรทำก่อนที่จะใช้ค่าที่
TripeHound

-1

ใช่มันจะกลับมาทรูเสมอยกเว้นถ้ามันน่าน ถ้าค่าตัวแปรเป็นNaNจะส่งคืนค่าFalseเสมอ!

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