1.0 เป็นเอาต์พุตที่ถูกต้องจาก std :: create_canonical หรือไม่


124

ฉันคิดเสมอว่าตัวเลขสุ่มจะอยู่ระหว่างศูนย์และหนึ่งโดยที่ไม่ใช่1ตัวเลขจากช่วงครึ่งเปิด [0,1) documention บน cppreference.comของstd::generate_canonicalยืนยันนี้

อย่างไรก็ตามเมื่อฉันเรียกใช้โปรแกรมต่อไปนี้:

#include <iostream>
#include <limits>
#include <random>

int main()
{
    std::mt19937 rng;

    std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    rng.seed(sequence);
    rng.discard(12 * 629143 + 6);

    float random = std::generate_canonical<float,
                   std::numeric_limits<float>::digits>(rng);

    if (random == 1.0f)
    {
        std::cout << "Bug!\n";
    }

    return 0;
}

มันให้ผลลัพธ์ต่อไปนี้:

Bug!

นั่นคือมันทำให้ฉันสมบูรณ์แบบ1ซึ่งทำให้เกิดปัญหาในการรวม MC ของฉัน เป็นพฤติกรรมที่ถูกต้องหรือมีข้อผิดพลาดในฝั่งของฉัน? สิ่งนี้ให้ผลลัพธ์เดียวกันกับ G ++ 4.7.3

g++ -std=c++11 test.c && ./a.out

และเสียงดัง 3.3

clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out

หากเป็นพฤติกรรมที่ถูกต้องฉันจะหลีกเลี่ยงได้1อย่างไร?

แก้ไข 1 : G ++ จาก git ดูเหมือนจะประสบปัญหาเดียวกัน ฉันอยู่

commit baf369d7a57fb4d0d5897b02549c3517bb8800fd
Date:   Mon Sep 1 08:26:51 2014 +0000

และการรวบรวมด้วย~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.outจะให้ผลลัพธ์เดียวกันlddให้ผลตอบแทน

linux-vdso.so.1 (0x00007fff39d0d000)
libstdc++.so.6 => /home/cschwan/temp/prefix/lib64/libstdc++.so.6 (0x00007f123d785000)
libm.so.6 => /lib64/libm.so.6 (0x000000317ea00000)
libgcc_s.so.1 => /home/cschwan/temp/prefix/lib64/libgcc_s.so.1 (0x00007f123d54e000)
libc.so.6 => /lib64/libc.so.6 (0x000000317e600000)
/lib64/ld-linux-x86-64.so.2 (0x000000317e200000)

แก้ไข 2 : ฉันรายงานพฤติกรรมที่นี่: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176

แก้ไข 3 : ทีมเสียงดังดูเหมือนจะตระหนักถึงปัญหา: http://llvm.org/bugs/show_bug.cgi?id=18767


21
@David Lively 1.f == 1.fในทุกกรณี (ทุกกรณีมีอะไรบ้างฉันไม่เห็นตัวแปรใด ๆ เลย1.f == 1.fมีเพียงกรณีเดียวที่นี่: 1.f == 1.fและนั่นก็คงเส้นคงวาtrue) โปรดอย่าเผยแพร่ตำนานนี้ต่อไป การเปรียบเทียบจุดลอยเป็นสิ่งที่แน่นอนเสมอ
R. Martinho Fernandes

15
@DavidLively: ไม่มันไม่ได้ การเปรียบเทียบจะแน่นอนเสมอ เป็นตัวถูกดำเนินการของคุณที่อาจไม่ถูกต้องหากมีการคำนวณไม่ใช่ตามตัวอักษร
Lightness Races ใน Orbit

2
@Galik จำนวนบวกใด ๆ ที่ต่ำกว่า 1.0 เป็นผลลัพธ์ที่ถูกต้อง 1.0 ไม่ใช่ ง่ายๆแค่นั้นเอง การปัดเศษไม่เกี่ยวข้อง: รหัสได้รับตัวเลขสุ่มและไม่ทำการปัดเศษใด ๆ
R.Martinho Fernandes

7
@DavidLively เขาบอกว่ามีค่าเดียวที่เปรียบเทียบเท่ากับ 1.0 ค่านั้นคือ 1.0 ค่าที่ใกล้เคียงกับ 1.0 จะไม่เปรียบเทียบเท่ากับ 1.0 ไม่สำคัญว่าฟังก์ชันการสร้างจะทำอะไร: หากส่งคืน 1.0 จะเปรียบเทียบเท่ากับ 1.0 หากไม่คืนค่า 1.0 จะไม่เปรียบเทียบเท่ากับ 1.0 ตัวอย่างของคุณโดยใช้การabs(random - 1.f) < numeric_limits<float>::epsilonตรวจสอบว่าผลลัพธ์ใกล้เคียงกับ 1.0หรือไม่ซึ่งผิดโดยสิ้นเชิงในบริบทนี้: มีตัวเลขที่ใกล้เคียงกับ 1.0 ซึ่งเป็นผลลัพธ์ที่ถูกต้องที่นี่กล่าวคือทั้งหมดที่มีค่าน้อยกว่า 1.0
R.Martinho Fernandes

4
@Galik ใช่จะมีปัญหาในการดำเนินการดังกล่าว แต่ปัญหานั้นอยู่ที่ผู้ปฏิบัติจะต้องจัดการ ผู้ใช้ต้องไม่เห็น 1.0 และผู้ใช้จะต้องเห็นการแจกแจงผลลัพธ์ทั้งหมดที่เท่ากันเสมอ
R. Martinho Fernandes

คำตอบ:


121

ปัญหาอยู่ในการแมปจาก codomain ของstd::mt19937( std::uint_fast32_t) ถึงfloat; อัลกอริทึมที่อธิบายโดยมาตรฐานให้ผลลัพธ์ที่ไม่ถูกต้อง (ไม่สอดคล้องกับคำอธิบายของผลลัพธ์ของอัลกอริทึม) เมื่อการสูญเสียความแม่นยำเกิดขึ้นหากโหมดการปัดเศษ IEEE754 ปัจจุบันเป็นสิ่งอื่นที่ไม่ใช่แบบกลมถึงลบ - อินฟินิตี้ (โปรดทราบว่าค่าเริ่มต้นคือรอบ -to- ใกล้ที่สุด)

เอาต์พุต 7549723 ของ mt19937 พร้อมเมล็ดพันธุ์ของคุณคือ 4294967257 ( 0xffffffd9u) ซึ่งเมื่อปัดเป็นโฟลต 32 บิตจะให้0x1p+32ซึ่งเท่ากับค่าสูงสุดของ mt19937, 4294967295 ( 0xffffffffu) เมื่อปัดเป็นโฟลต 32 บิตด้วย

มาตรฐานสามารถตรวจสอบพฤติกรรมที่ถูกต้องได้หากมีการระบุว่าเมื่อแปลงจากเอาต์พุตของ URNG เป็นRealTypeของgenerate_canonicalการปัดเศษจะต้องดำเนินการไปยังอินฟินิตี้เชิงลบ สิ่งนี้จะให้ผลลัพธ์ที่ถูกต้องในกรณีนี้ ในฐานะ QOI มันจะเป็นการดีสำหรับ libstdc ++ ในการเปลี่ยนแปลงนี้

ด้วยการเปลี่ยนแปลงนี้1.0จะไม่ถูกสร้างขึ้นอีกต่อไป แทนค่าขอบเขต0x1.fffffep-Nสำหรับ0 < N <= 8จะถูกสร้างบ่อยขึ้น (โดยประมาณ2^(8 - N - 32)ต่อNขึ้นอยู่กับการแจกแจงจริงของ MT19937)

ฉันอยากจะแนะนำว่าอย่าใช้floatกับstd::generate_canonicalโดยตรง ค่อนข้างสร้างจำนวนในdoubleแล้วปัดเศษเป็นค่าอนันต์ลบ:

    double rd = std::generate_canonical<double,
        std::numeric_limits<float>::digits>(rng);
    float rf = rd;
    if (rf > rd) {
      rf = std::nextafter(rf, -std::numeric_limits<float>::infinity());
    }

ปัญหานี้อาจเกิดขึ้นกับstd::uniform_real_distribution<float>; การแก้ปัญหาคือเดียวกันจะมีความเชี่ยวชาญการจัดจำหน่ายในและรอบผลต่ออินฟินิตี้ลบในdoublefloat


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

2
@supercat: หากต้องการพูดนอกเรื่องเล็กน้อยมีเหตุผลที่ดีที่จะพยายามทำให้ฟังก์ชันไซน์แม่นยำที่สุดสำหรับมุมเล็ก ๆ เช่นเนื่องจากข้อผิดพลาดเล็ก ๆ ใน sin (x) สามารถเปลี่ยนเป็นข้อผิดพลาดขนาดใหญ่ใน sin (x) / x (ซึ่งเกิดขึ้นบ่อยครั้งในการคำนวณในโลกแห่งความจริง) เมื่อ x เข้าใกล้ศูนย์ "ความแม่นยำพิเศษ" ใกล้ทวีคูณของπโดยทั่วไปเป็นเพียงผลข้างเคียงของสิ่งนั้น
Ilmari Karonen

1
@IlmariKaronen: สำหรับมุมเล็ก ๆ ที่เพียงพอ sin (x) คือ x squawk ของฉันที่ฟังก์ชันไซน์ของ Java เกี่ยวข้องกับมุมที่อยู่ใกล้กับทวีคูณของ pi ฉันคิดว่า 99% ของเวลาที่รหัสถามหาsin(x)สิ่งที่ต้องการจริงๆคือไซน์ของ (π / Math.PI) คูณ x คนที่ดูแล Java ยืนยันว่าจะดีกว่าถ้ามีรายงานกิจวัตรทางคณิตศาสตร์ที่ช้าว่าไซน์ของ Math.PI คือความแตกต่างระหว่างπและ Math.PI มากกว่าที่จะรายงานค่าที่น้อยกว่าเล็กน้อยแม้ว่าใน 99% ของแอปพลิเคชันนั้น จะดีกว่า ...
supercat

3
@ecatmur ข้อเสนอแนะ; อัปเดตโพสต์นี้เพื่อพูดถึงว่าstd::uniform_real_distribution<float>มีปัญหาเดียวกันอันเป็นผลมาจากสิ่งนี้ (เพื่อให้คนที่ค้นหา uniform_real_distribution จะมี Q / A นี้ขึ้นมา)
MM

1
@ecatmur ฉันไม่แน่ใจว่าทำไมคุณถึงต้องการปัดเศษเป็นค่าอนันต์เชิงลบ เนื่องจากgenerate_canonicalควรสร้างตัวเลขในช่วง[0,1)และเรากำลังพูดถึงข้อผิดพลาดที่สร้าง 1.0 ในบางครั้งการปัดเศษเป็นศูนย์จะไม่ได้ผลเท่าหรือ?
Marshall Clow

39

ตามมาตรฐาน1.0ไม่ถูกต้อง

C ++ 11 §26.5.7.2เทมเพลตฟังก์ชั่น create_canonical

แต่ละฟังก์ชันสร้างอินสแตนซ์จากเทมเพลตที่อธิบายไว้ในส่วนนี้ 26.5.7.2 จะแมปผลลัพธ์ของการเรียกใช้หนึ่งหรือมากกว่าของตัวสร้างตัวเลขสุ่มแบบสม่ำเสมอที่gให้มากับสมาชิกหนึ่งคนของ RealType ที่ระบุดังนั้นหากค่าที่ฉันสร้างขึ้นโดยgมีการกระจายอย่างสม่ำเสมอ ผลลัพธ์ของการสร้างอินสแตนซ์ t j , 0 ≤ t j <1มีการกระจายอย่างสม่ำเสมอที่สุดเท่าที่จะทำได้ตามที่ระบุไว้ด้านล่าง


25
+1 ฉันไม่เห็นข้อบกพร่องใด ๆ ในโปรแกรมของ OP ดังนั้นฉันจึงเรียกสิ่งนี้ว่าข้อผิดพลาด libstdc ++ และ libc ++ ... ซึ่งดูเหมือนจะไม่น่าเป็นไปได้เล็กน้อย แต่เราไปแล้ว
Lightness Races ใน Orbit

-2

ฉันเพิ่งเจอคำถามที่คล้ายกันuniform_real_distributionและนี่คือวิธีที่ฉันตีความถ้อยคำที่ไม่เหมาะสมของ Standard ในหัวข้อ:

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

มาตรฐานระบุว่าทั้งสองuniform_real_distribution<T>(0,1)(g)และgenerate_canonical<T,1000>(g)ควรคืนค่าในช่วงครึ่งเปิด [0,1) แต่นี่คือค่าทางคณิตศาสตร์ เมื่อคุณใช้เวลาเป็นจำนวนจริงในช่วงครึ่งเปิด [0,1) และแสดงเป็น IEEE floating-point, ดี, T(1.0)ส่วนที่สำคัญของเวลามันจะรอบขึ้นไป

เมื่อTใดfloat(24 mantissa bits) เราคาดว่าจะเห็นuniform_real_distribution<float>(0,1)(g) == 1.0fประมาณ 1 ใน 2 ^ 25 ครั้ง การทดลองพลังเดรัจฉานของฉันกับ libc ++ ยืนยันความคาดหวังนี้

template<class F>
void test(long long N, const F& get_a_float) {
    int count = 0;
    for (long long i = 0; i < N; ++i) {
        float f = get_a_float();
        if (f == 1.0f) {
            ++count;
        }
    }
    printf("Expected %d '1.0' results; got %d in practice\n", (int)(N >> 25), count);
}

int main() {
    std::mt19937 g(std::random_device{}());
    auto N = (1uLL << 29);
    test(N, [&g]() { return std::uniform_real_distribution<float>(0,1)(g); });
    test(N, [&g]() { return std::generate_canonical<float, 32>(g); });
}

ตัวอย่างผลลัพธ์:

Expected 16 '1.0' results; got 19 in practice
Expected 16 '1.0' results; got 11 in practice

เมื่อTใดdouble(53 แมนทิสซาบิต) เราคาดว่าจะเห็นuniform_real_distribution<double>(0,1)(g) == 1.0ประมาณ 1 ใน 2 ^ 54 ครั้ง ฉันไม่มีความอดทนที่จะทดสอบความคาดหวังนี้ :)

ความเข้าใจของฉันคือพฤติกรรมนี้ดี มันอาจจะกลายเป็นความรู้สึกของ "ครึ่งเปิด rangeness" ของเราว่าการกระจายที่อ้างว่าจะกลับหมายเลข "น้อยกว่า 1.0" สามารถในตัวเลขผลตอบแทนจากความเป็นจริงที่มีความเท่าเทียมกันในการ1.0; แต่นี่คือสองความหมายที่แตกต่างกันของ "1.0" ดูไหม อย่างแรกคือ1.0 ทางคณิตศาสตร์ สองคือ IEEE 1.0แม่นยำเดียวจำนวนจุดลอยตัว และเราได้รับการสอนมาหลายทศวรรษแล้วว่าอย่าเปรียบเทียบตัวเลขทศนิยมเพื่อความเท่าเทียมกัน

อัลกอริทึมใดก็ตามที่คุณป้อนตัวเลขสุ่มเข้าไปจะไม่สนใจว่าบางครั้งจะได้รับตรง1.0หรือไม่ ไม่มีอะไรที่คุณสามารถทำได้กับตัวเลขทศนิยมยกเว้นการดำเนินการทางคณิตศาสตร์และทันทีที่คุณดำเนินการทางคณิตศาสตร์โค้ดของคุณจะต้องจัดการกับการปัดเศษ แม้ว่าคุณจะสามารถสรุปได้อย่างถูกต้องตามกฎหมายgenerate_canonical<float,1000>(g) != 1.0fแต่คุณก็ยังไม่สามารถสรุปได้generate_canonical<float,1000>(g) + 1.0f != 2.0f- เนื่องจากการปัดเศษ คุณไม่สามารถไปจากมันได้ แล้วทำไมเราจะแกล้งทำเป็นว่าคุณทำได้?


2
ฉันไม่เห็นด้วยอย่างยิ่งกับมุมมองนี้ หากมาตรฐานกำหนดค่าจากช่วงครึ่งเปิดและการใช้งานทำลายกฎนี้การใช้งานจะไม่ถูกต้อง น่าเสียดายที่ ecatmur ชี้ให้เห็นอย่างถูกต้องในคำตอบของเขามาตรฐานนี้ยังกำหนดอัลกอริทึมที่มีข้อบกพร่อง สิ่งนี้ได้รับการยอมรับอย่างเป็นทางการที่นี่: open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan: การตีความของฉันคือการใช้งานไม่ได้ผิดกฎ มาตรฐานกำหนดค่าจาก [0,1); การนำไปใช้คืนค่าจาก [0,1); ค่าเหล่านี้บางส่วนเกิดขึ้นเพื่อปัดเศษเป็น IEEE 1.0fแต่นั่นเป็นสิ่งที่หลีกเลี่ยงไม่ได้เมื่อคุณส่งไปที่ IEEE หากคุณต้องการผลลัพธ์ทางคณิตศาสตร์ที่แท้จริงให้ใช้ระบบการคำนวณเชิงสัญลักษณ์ หากคุณพยายามใช้จุดลอยตัวของ IEEE เพื่อแสดงตัวเลขที่อยู่ภายในeps1 แสดงว่าคุณอยู่ในสถานะบาป
Quuxplusone

ตัวอย่างเช่นสมมุติว่าก็จะถูกทำลายโดยข้อผิดพลาดนี้: canonical - 1.0fสิ่งที่หารด้วย ทุก ๆ ลอยซึ่งแสดงใน[0, 1.0), x-1.0fเป็นที่ไม่ใช่ศูนย์ ด้วย 1.0f คุณจะได้หารด้วยศูนย์แทนที่จะเป็นตัวหารเล็ก ๆ
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.