ควรเพิ่มการลอยตัวในลำดับใดเพื่อให้ได้ผลลัพธ์ที่แม่นยำที่สุด


105

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

หากเรามีฟังก์ชันบางอย่างซึ่งสะสมตัวเลขทศนิยม:

std::accumulate(v.begin(), v.end(), 0.0);

vเป็นstd::vector<float>ตัวอย่างเช่น

  • จะดีกว่าไหมหากเรียงลำดับตัวเลขเหล่านี้ก่อนสะสม

  • คำสั่งใดที่จะให้คำตอบที่แม่นยำที่สุด

ฉันสงสัยว่าการเรียงลำดับตัวเลขจากน้อยไปมากจะทำให้ข้อผิดพลาดของตัวเลขน้อยลงแต่น่าเสียดายที่ฉันไม่สามารถพิสูจน์ได้ด้วยตัวเอง

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


17
สิ่งนี้มีทุกอย่างที่เกี่ยวข้องกับการเขียนโปรแกรมในโลกแห่งความเป็นจริง อย่างไรก็ตามแอปพลิเคชั่นจำนวนมากไม่สนใจเกี่ยวกับความแม่นยำที่ดีที่สุดของการคำนวณตราบใดที่มัน 'ค่อนข้างใกล้' งานวิศวกรรม? สำคัญมาก ๆ. การใช้งานทางการแพทย์? สำคัญมาก ๆ. สถิติขนาดใหญ่? ยอมรับความแม่นยำได้น้อยกว่า
Zéychin

18
โปรดอย่าตอบเว้นแต่คุณจะรู้จริงและสามารถชี้ไปที่หน้าที่อธิบายเหตุผลของคุณโดยละเอียด มีเรื่องไร้สาระมากมายเกี่ยวกับตัวเลขจุดลอยตัวที่บินอยู่รอบ ๆ เราไม่ต้องการเพิ่มเข้าไป ถ้าคุณคิดว่าคุณรู้. หยุด. เพราะถ้าคุณแค่คิดว่าคุณรู้แล้วคุณอาจคิดผิด
Martin York

4
@ Zéychin "วิศวกรรมการใช้งานสำคัญมากการใช้งานทางการแพทย์สำคัญมาก" ??? ฉันคิดว่าคุณจะแปลกใจถ้าคุณรู้ความจริง :)
BЈовић

3
@Zeychin ข้อผิดพลาด Absolute ไม่เกี่ยวข้อง สิ่งที่สำคัญคือข้อผิดพลาดสัมพัทธ์ ถ้าไม่กี่ในร้อยของเรเดียนเท่ากับ 0.001% แล้วใครจะสนล่ะ?
BЈовић

3
ฉันแนะนำให้อ่านนี้: "สิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนต้องรู้เกี่ยวกับจุดลอยตัว" perso.ens-lyon.fr/jean-michel.muller/goldberg.pdf
Mohammad Alaggan

คำตอบ:


107

โดยพื้นฐานแล้วสัญชาตญาณของคุณนั้นถูกต้องการเรียงลำดับจากน้อยไปมาก (ของขนาด) มักจะช่วยปรับปรุงสิ่งต่างๆได้บ้าง พิจารณากรณีที่เราเพิ่มการลอยตัวแบบ single-precision (32 บิต) และมีค่า 1 พันล้านเท่ากับ 1 / (1 พันล้าน) และค่าหนึ่งเท่ากับ 1 ถ้าค่า 1 มาก่อนผลรวมจะมา ถึง 1 เนื่องจาก 1 + (1/1 พันล้าน) เป็น 1 เนื่องจากสูญเสียความแม่นยำ การเพิ่มแต่ละครั้งไม่มีผลต่อยอดรวมทั้งหมด

ถ้าค่าเล็ก ๆ มาก่อนอย่างน้อยก็จะรวมกับบางสิ่งแม้ว่าฉันจะมี 2 ^ 30 ของค่านั้นในขณะที่หลังจาก 2 ^ 25 หรือมากกว่านั้นฉันก็กลับมาอยู่ในสถานการณ์ที่แต่ละค่าไม่ส่งผลกระทบต่อยอดรวม ๆ ๆ ๆ ดังนั้นฉันยังคงต้องการเทคนิคเพิ่มเติม

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

ถึงกระนั้นหากมีตัวเลขเชิงลบเข้ามาเกี่ยวข้องก็ง่ายที่จะ "ชิงไหวชิงพริบ" แนวทางนี้ {1, -1, 1 billionth}พิจารณาสามค่าที่จะสรุป, ผลรวมที่ถูกต้องคือ arithmetically 1 billionthแต่ถ้านอกเหนือจากครั้งแรกของฉันที่เกี่ยวข้องกับค่าเล็ก ๆ แล้วผลรวมสุดท้ายของฉันจะเป็น 0 ใน 6 คำสั่งที่เป็นไปได้เพียง 2 "ถูกต้อง" - และ{1, -1, 1 billionth} {-1, 1, 1 billionth}คำสั่งซื้อทั้ง 6 คำสั่งให้ผลลัพธ์ที่แม่นยำในระดับของค่าขนาดที่ใหญ่ที่สุดในอินพุต (0.0000001% ออก) แต่สำหรับ 4 คำสั่งนั้นผลลัพธ์ไม่ถูกต้องในระดับของโซลูชันจริง (100% จาก 100%) ปัญหาเฉพาะที่คุณกำลังแก้ไขจะบอกคุณได้ว่าอดีตนั้นดีพอหรือไม่

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

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


8
+1 สำหรับคำอธิบาย สิ่งนี้ค่อนข้างสวนทางกับธรรมชาติเนื่องจากการบวกมักจะคงที่ในเชิงตัวเลข (ไม่เหมือนกับการลบและการหาร)
Konrad Rudolph

2
@ Konrad มันอาจจะมีความเสถียรในเชิงตัวเลข แต่ก็ไม่แม่นยำเนื่องจากขนาดของตัวถูกดำเนินการต่างกัน :)
MSN

3
@ 6502: เรียงตามลำดับขนาดดังนั้น -1 จึงอยู่ท้ายสุด หากมูลค่าที่แท้จริงของผลรวมมีขนาด 1 ก็ไม่เป็นไร หากคุณรวมค่าสามค่าเข้าด้วยกัน: 1 / billion, 1 และ -1 คุณจะได้ 0 ณ จุดนี้คุณต้องตอบคำถามเชิงปฏิบัติที่น่าสนใจ - คุณต้องการคำตอบที่ถูกต้องในระดับของ ผลรวมจริงหรือคุณต้องการเพียงคำตอบที่ถูกต้องตามมาตราส่วนของค่าที่มากที่สุด? สำหรับการใช้งานจริงวิธีหลังนี้ดีพอ แต่เมื่อคุณไม่ต้องการแนวทางที่ซับซ้อนกว่านี้ ฟิสิกส์ควอนตัมใช้การทำให้เป็นปกติ
Steve Jessop

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

2
@Kevin Panko: รุ่นที่เรียบง่ายคือ single-precision float มี 24 ไบนารีหลักซึ่งมากที่สุดคือบิตที่ตั้งไว้ที่ใหญ่ที่สุดในจำนวน ดังนั้นหากคุณบวกตัวเลขสองตัวที่มีขนาดต่างกันมากกว่า 2 ^ 24 คุณจะสูญเสียค่าที่น้อยกว่าทั้งหมดและหากขนาดต่างกันในระดับที่น้อยกว่าคุณจะสูญเสียจำนวนบิตที่สอดคล้องกันของค่าที่เล็กกว่า จำนวน.
Steve Jessop

88

นอกจากนี้ยังมีอัลกอริทึมที่ออกแบบมาสำหรับการดำเนินการสะสมประเภทนี้เรียกว่าKahan Summationที่คุณควรทราบ

อ้างอิงจาก Wikipedia

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

ใน pseudocode อัลกอริทึมคือ:

function kahanSum(input)
 var sum = input[1]
 var c = 0.0          //A running compensation for lost low-order bits.
 for i = 2 to input.length
  y = input[i] - c    //So far, so good: c is zero.
  t = sum + y         //Alas, sum is big, y small, so low-order digits of y are lost.
  c = (t - sum) - y   //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
  sum = t             //Algebraically, c should always be zero. Beware eagerly optimising compilers!
 next i               //Next time around, the lost low part will be added to y in a fresh attempt.
return sum

3
+1 เพิ่มเติมที่น่ารักสำหรับกระทู้นี้ คอมไพเลอร์ใด ๆ ที่ "เพิ่มประสิทธิภาพอย่างกระตือรือร้น" ข้อความเหล่านั้นควรถูกแบน
Chris A.

1
เป็นวิธีง่ายๆในการเพิ่มความแม่นยำเกือบสองเท่าโดยใช้ตัวแปรผลรวมสองตัวsumและcขนาดที่ต่างกัน สามารถขยายไปยังตัวแปร N ได้เล็กน้อย
MSalters

2
@ChrisA. คุณสามารถควบคุมสิ่งนี้ได้อย่างชัดเจนในคอมไพเลอร์ทั้งหมดที่นับ (เช่นผ่านทาง-ffast-mathGCC)
Konrad Rudolph

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

การใช้ตัวแปรความแม่นยำสองเท่าsum, c, t, yจะช่วยได้ คุณต้องเพิ่มsum -= cก่อนreturn sumด้วย
G. โคเฮน

34

ฉันลองใช้ตัวอย่างที่รุนแรงในคำตอบที่ Steve Jessop ให้มา

#include <iostream>
#include <iomanip>
#include <cmath>

int main()
{
    long billion = 1000000000;
    double big = 1.0;
    double small = 1e-9;
    double expected = 2.0;

    double sum = big;
    for (long i = 0; i < billion; ++i)
        sum += small;
    std::cout << std::scientific << std::setprecision(1) << big << " + " << billion << " * " << small << " = " <<
        std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    sum = 0;
    for (long i = 0; i < billion; ++i)
        sum += small;
    sum += big;
    std::cout  << std::scientific << std::setprecision(1) << billion << " * " << small << " + " << big << " = " <<
        std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    return 0;
}

ฉันได้ผลลัพธ์ดังต่อไปนี้:

1.0e+00 + 1000000000 * 1.0e-09 = 2.000000082740371    (difference = 0.000000082740371)
1000000000 * 1.0e-09 + 1.0e+00 = 1.999999992539933    (difference = 0.000000007460067)

ข้อผิดพลาดในบรรทัดแรกจะใหญ่กว่าในบรรทัดที่สองสิบเท่า

หากฉันเปลี่ยนdoubles เป็นfloats ในโค้ดด้านบนฉันจะได้รับ:

1.0e+00 + 1000000000 * 1.0e-09 = 1.000000000000000    (difference = 1.000000000000000)
1000000000 * 1.0e-09 + 1.0e+00 = 1.031250000000000    (difference = 0.968750000000000)

ไม่มีคำตอบใดที่ใกล้เคียงกับ 2.0 (แต่ข้อที่สองใกล้กว่าเล็กน้อย)

การใช้การสรุป Kahan (พร้อมdoubles) ตามที่ Daniel Pryden อธิบายไว้:

#include <iostream>
#include <iomanip>
#include <cmath>

int main()
{
    long billion = 1000000000;
    double big = 1.0;
    double small = 1e-9;
    double expected = 2.0;

    double sum = big;
    double c = 0.0;
    for (long i = 0; i < billion; ++i) {
        double y = small - c;
        double t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }

    std::cout << "Kahan sum  = " << std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    return 0;
}

ฉันได้รับ 2.0:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

และแม้ว่าฉันจะเปลี่ยนdoubles เป็นfloats ในโค้ดด้านบนฉันจะได้รับ:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

ดูเหมือนว่า Kahan จะเป็นหนทางไป!


ค่า "ใหญ่" ของฉันเท่ากับ 1 ไม่ใช่ 1e9 คำตอบที่สองของคุณซึ่งเพิ่มตามลำดับขนาดที่เพิ่มขึ้นนั้นถูกต้องทางคณิตศาสตร์ (1 พันล้านบวกหนึ่งพันล้านพันล้านคือ 1 พันล้านและ 1) แม้ว่าวิธีนี้จะโชคดีกว่าก็ตาม :-) โปรดทราบdoubleว่าไม่ได้รับผลเสีย การสูญเสียความแม่นยำในการรวมกันเป็นพันล้านในพันล้านเนื่องจากมี 52 บิตที่สำคัญในขณะที่ IEEE floatมีเพียง 24 และจะ
Steve Jessop

@ สตีฟข้อผิดพลาดของฉันขอโทษ ฉันได้อัปเดตโค้ดตัวอย่างตามที่คุณต้องการแล้ว
Andrew Stein

4
Kahan ยังคงมีความแม่นยำที่ จำกัด แต่ในการสร้างเคสนักฆ่าคุณต้องมีทั้งผลรวมหลักและตัวสะสมข้อผิดพลาดcเพื่อให้มีค่าที่ใหญ่กว่า summand ถัดไปมาก ซึ่งหมายความว่าผลรวมมีค่าน้อยกว่าผลรวมหลักมากดังนั้นจะต้องมีจำนวนมากที่น่ากลัวในการบวกมาก โดยเฉพาะอย่างยิ่งกับdoubleเลขคณิต
Steve Jessop

14

มีคลาสของอัลกอริทึมที่แก้ปัญหานี้ได้โดยไม่จำเป็นต้องเรียงลำดับหรือเรียงลำดับข้อมูลใหม่ใหม่

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

นี่คือบทคัดย่อของเอกสารล่าสุด:

เรานำเสนออัลกอริทึมออนไลน์แบบใหม่สำหรับการสรุปกระแสของตัวเลขทศนิยม โดย "ออนไลน์" เราหมายความว่าอัลกอริทึมจำเป็นต้องเห็นอินพุตเพียงครั้งละหนึ่งรายการและสามารถรับกระแสอินพุตที่มีความยาวตามอำเภอใจของอินพุตดังกล่าวในขณะที่ต้องการหน่วยความจำคงที่เท่านั้น โดย“ แน่นอน” เราหมายความว่าผลรวมของอาร์เรย์ภายในของอัลกอริทึมของเรานั้นเท่ากับผลรวมของอินพุตทั้งหมดและผลลัพธ์ที่ส่งกลับคือผลรวมที่ปัดเศษอย่างถูกต้อง การพิสูจน์ความถูกต้องนั้นใช้ได้สำหรับอินพุตทั้งหมด (รวมถึงตัวเลขที่ไม่เป็นมาตรฐาน แต่มีการโอเวอร์โฟลว์ระดับกลางของโมดูโล) และไม่ขึ้นกับจำนวน summands หรือหมายเลขเงื่อนไขของผลรวม อัลกอริทึมที่ไม่มีอาการต้องการเพียง 5 FLOP ต่อ summand และเนื่องจากการขนานกันในระดับคำสั่งทำงานช้ากว่าที่เห็นได้ชัดประมาณ 2-3 เท่า ลูป "การสรุปซ้ำแบบวนซ้ำธรรมดา" ที่รวดเร็ว แต่โง่เมื่อจำนวน summand มากกว่า 10,000 ดังนั้นสำหรับความรู้ของเรามันเป็นหน่วยความจำที่เร็วที่สุดแม่นยำที่สุดและมีประสิทธิภาพมากที่สุดในบรรดาอัลกอริทึมที่รู้จักกัน อันที่จริงเป็นเรื่องยากที่จะดูว่าอัลกอริทึมที่เร็วขึ้นหรือหนึ่งที่ต้องการ FLOP น้อยลงอย่างมีนัยสำคัญจะเกิดขึ้นได้อย่างไรหากไม่มีการปรับปรุงฮาร์ดแวร์ มีแอปพลิเคชันสำหรับ summands จำนวนมาก

ที่มา: อัลกอริทึม 908: ออนไลน์สรุปที่แน่นอนของ Floating-Point Streams


1
@ ตรงกันข้าม: ยังคงมีห้องสมุดอิฐและปูนอยู่รอบ ๆ หรืออีกวิธีหนึ่งคือการซื้อ PDF ออนไลน์มีราคา $ 5 - $ 15 (ขึ้นอยู่กับว่าคุณเป็นสมาชิก ACM หรือไม่) สุดท้าย DeepDyve ดูเหมือนจะเสนอให้ยืมกระดาษเป็นเวลา 24 ชั่วโมงในราคา $ 2.99 (หากคุณเพิ่งเริ่มใช้ DeepDyve คุณอาจจะได้รับฟรีเป็นส่วนหนึ่งของการทดลองใช้ฟรี): deepdyve.com/lp/acm / …
NPE

2

จากคำตอบของ Steve ในการเรียงลำดับตัวเลขจากน้อยไปหามากฉันขอแนะนำอีกสองแนวคิด:

  1. ตัดสินใจเกี่ยวกับความแตกต่างของเลขชี้กำลังของตัวเลขสองตัวข้างต้นซึ่งคุณอาจตัดสินใจว่าคุณจะสูญเสียความแม่นยำมากเกินไป

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

คุณทำซ้ำขั้นตอนด้วยคิวชั่วคราว (เรียงลำดับแล้ว) และอาจมีความแตกต่างมากกว่าในเลขชี้กำลัง

ฉันคิดว่านี่จะค่อนข้างช้าถ้าคุณต้องคำนวณเลขชี้กำลังตลอดเวลา

ฉันไปกับโปรแกรมอย่างรวดเร็วและผลลัพธ์คือ 1.99903


2

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

while the list has multiple elements
    remove the two smallest elements from the list
    add them and put the result back in
the single element in the list is the result

แน่นอนว่าอัลกอริทึมนี้จะมีประสิทธิภาพสูงสุดโดยใช้ลำดับความสำคัญแทนที่จะเป็นรายการ รหัส C ++:

template <typename Queue>
void reduce(Queue& queue)
{
    typedef typename Queue::value_type vt;
    while (queue.size() > 1)
    {
        vt x = queue.top();
        queue.pop();
        vt y = queue.top();
        queue.pop();
        queue.push(x + y);
    }
}

คนขับ:

#include <iterator>
#include <queue>

template <typename Iterator>
typename std::iterator_traits<Iterator>::value_type
reduce(Iterator begin, Iterator end)
{
    typedef typename std::iterator_traits<Iterator>::value_type vt;
    std::priority_queue<vt> positive_queue;
    positive_queue.push(0);
    std::priority_queue<vt> negative_queue;
    negative_queue.push(0);
    for (; begin != end; ++begin)
    {
        vt x = *begin;
        if (x < 0)
        {
            negative_queue.push(x);
        }
        else
        {
            positive_queue.push(-x);
        }
    }
    reduce(positive_queue);
    reduce(negative_queue);
    return negative_queue.top() - positive_queue.top();
}

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


2

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

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


0

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

กล่าวได้ว่าคุณสามารถทำได้ดีขึ้นโดยการติดตามผลรวมบางส่วนที่ไม่ทับซ้อนกันหลาย ๆ นี่คือเอกสารที่อธิบายเทคนิคและการนำเสนอการพิสูจน์ความถูกต้อง: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps

อัลกอริทึมและวิธีการอื่น ๆ ในการสรุปจุดลอยตัวที่แน่นอนถูกนำไปใช้ใน Python อย่างง่ายที่: http://code.activestate.com/recipes/393090/ อย่างน้อยสองอย่างนั้นสามารถแปลงเป็น C ++ ได้เล็กน้อย


0

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

/* clear array */
void clearsum(float asum[256])
{
size_t i;
    for(i = 0; i < 256; i++)
        asum[i] = 0.f;
}

/* add a number into array */
void addtosum(float f, float asum[256])
{
size_t i;
    while(1){
        /* i = exponent of f */
        i = ((size_t)((*(unsigned int *)&f)>>23))&0xff;
        if(i == 0xff){          /* max exponent, could be overflow */
            asum[i] += f;
            return;
        }
        if(asum[i] == 0.f){     /* if empty slot store f */
            asum[i] = f;
            return;
        }
        f += asum[i];           /* else add slot to f, clear slot */
        asum[i] = 0.f;          /* and continue until empty slot */
    }
}

/* return sum from array */
float returnsum(float asum[256])
{
float sum = 0.f;
size_t i;
    for(i = 0; i < 256; i++)
        sum += asum[i];
    return sum;
}

ตัวอย่างความแม่นยำสองเท่า:

/* clear array */
void clearsum(double asum[2048])
{
size_t i;
    for(i = 0; i < 2048; i++)
        asum[i] = 0.;
}

/* add a number into array */
void addtosum(double d, double asum[2048])
{
size_t i;
    while(1){
        /* i = exponent of d */
        i = ((size_t)((*(unsigned long long *)&d)>>52))&0x7ff;
        if(i == 0x7ff){         /* max exponent, could be overflow */
            asum[i] += d;
            return;
        }
        if(asum[i] == 0.){      /* if empty slot store d */
            asum[i] = d;
            return;
        }
        d += asum[i];           /* else add slot to d, clear slot */
        asum[i] = 0.;           /* and continue until empty slot */
    }
}

/* return sum from array */
double returnsum(double asum[2048])
{
double sum = 0.;
size_t i;
    for(i = 0; i < 2048; i++)
        sum += asum[i];
    return sum;
}

ฟังดูคล้ายกับวิธีการของMalcolm 1971หรือมากกว่านั้นคือตัวแปรที่ใช้เลขชี้กำลังโดยDemmel และ Hida ("Algorithm 3") มีอัลกอริทึมอื่นที่ทำห่วงแบบพกพาเหมือนของคุณ แต่ฉันหาไม่พบในขณะนี้
ZachB

@ZachB - แนวคิดคล้ายกับการเรียงลำดับการผสานจากล่างขึ้นบนสำหรับรายการที่เชื่อมโยงซึ่งใช้อาร์เรย์ขนาดเล็กโดยที่อาร์เรย์ [i] ชี้ไปที่รายการด้วยโหนด 2 ^ i ฉันไม่รู้ว่าสิ่งนี้ย้อนกลับไปไกลแค่ไหน ในกรณีของฉันมันเป็นการค้นพบตัวเองในปี 1970
rcgldr

-1

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

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


1
“ นั่นจะทำให้คุณมีความแม่นยำมากขึ้นกว่าที่เทคนิคอื่น ๆ ทำได้” คุณรู้หรือไม่ว่าคำตอบของคุณเกิดขึ้นมากกว่าหนึ่งปีหลังจากคำตอบที่ล่าช้าก่อนหน้านี้ซึ่งอธิบายถึงวิธีการใช้การสรุปที่แน่นอน
Pascal Cuoq

ประเภท "long double" นั้นน่ากลัวและคุณไม่ควรใช้
Jeff

-1

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

((-1 + 1) + 1e-20) จะให้ 1e-20

แต่

((1e-20 + 1) - 1) จะให้ 0

ในสมการแรกที่ตัวเลขขนาดใหญ่สองตัวถูกยกเลิกในขณะที่ในวินาทีที่ 1e-20 เทอมจะหายไปเมื่อบวกกับ 1 เนื่องจากไม่มีความแม่นยำเพียงพอที่จะคงไว้

นอกจากนี้การสรุปแบบคู่ยังค่อนข้างดีสำหรับการรวมตัวเลขจำนวนมาก

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