Toplam siparişin değiştirilmesi neden farklı bir sonuç döndürüyor?


294

Toplam siparişin değiştirilmesi neden farklı bir sonuç döndürüyor?

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

Hem Java hem de JavaScript aynı sonuçları döndürür.

Kayan nokta sayılarının ikili olarak gösterilme şekli nedeniyle, bazı rasyonel sayıların ( 1/3 - 0.333333 ... gibi ) tam olarak temsil edilemediğini anlıyorum .

Neden öğelerin sırasını değiştirmek sonucu etkiler?


28
Reel sayıların toplamı ilişkisel ve değişmeli. Kayan noktalar gerçek sayılar değildir. Aslında operasyonlarının değişmeli olmadığını kanıtladınız. Onların da ilişkisel olmadıklarını göstermek oldukça kolaydır (örn. (2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1)). Bu nedenle, evet: toplamların sırasını ve diğer işlemleri seçerken dikkatli olun. Bazı diller "yüksek hassasiyetli" toplamları (ör. Python'lar math.fsum) gerçekleştirmek için yerleşik bir yöntem sunar , bu nedenle saf toplam algoritması yerine bu işlevleri kullanmayı düşünebilirsiniz.
Bakuriu

1
@RBerteig Bu, dilin aritmetik ifadeler için işlem sırasını inceleyerek belirlenebilir ve kayan nokta sayılarının bellekte gösterimi farklı değilse, operatör öncelik kuralları aynı ise sonuçlar aynı olacaktır. Dikkat edilmesi gereken başka bir nokta da: Bankacılık uygulamaları geliştiren geliştiricilerin bunu çözmesinin ne kadar sürdüğünü merak ediyorum? Bu ekstra 0000000000004 sent gerçekten toplanıyor !
Chris Cirefice

3
@ChrisCirefice: Eğer 0.00000004 varsa sent , yanlış yapıyoruz. Sen gerektiğini asla finansal hesaplamalar için bir ikili kayan nokta türü kullanın.
Daniel Pryden

2
@DanielPryden Ah ne yazık ki, bu bir şakaydı ... sadece bu tür bir problemi çözmesi gereken insanların bildiğiniz en önemli işlerden birine sahip olduğu, insanların parasal statüsünü elinde tuttuğu fikrini atmak . Çok
alaycıydım

Yanıtlar:


276

Belki bu soru aptalca, ama neden sadece öğelerin sırasını değiştirmek sonucu etkiliyor?

Büyüklüklerine göre değerlerin yuvarlandığı noktaları değiştirecektir. Bir örnek olarak ayni bizler gören, en yerine ikili kayan nokta, her bir ek için "sonsuz" hassas yapılır, sonra yuvarlanır 4 anlamlı basamak ile nokta tipi yüzen bir ondalık kullandığını farz edelim o şey temsil edilebilir en yakın sayı. İşte iki toplam:

1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
                = 1.000 + 0.6667 (no rounding needed!)
                = 1.667 (where 1.6667 is rounded to 1.667)

2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
                = 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
                = 1.666 (where 1.6663 is rounded to 1.666)

Bunun bir sorun olması için tamsayı olmayanlara bile ihtiyacımız yok:

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1

Bu, muhtemelen daha açık bir şekilde, önemli kısmın sınırlı sayıda önemli basamağa sahip olduğumuzu gösterir ; ondalık basamağa . Her zaman aynı sayıda ondalık basamak tutabilirsek, en azından toplama ve çıkarma ile iyi oluruz (değerler taşmadığı sürece). Sorun, daha büyük sayılara ulaştığınızda daha küçük bilgilerin kaybolmasıdır - bu durumda 10001, 10000'e yuvarlanır. (Bu, Eric Lippert'in cevabında belirttiği sorunun bir örneğidir .)

Sağ tarafın ilk satırındaki değerlerin her durumda aynı olduğunu belirtmek önemlidir - bu nedenle ondalık sayılarınızın (23.53, 5.88, 17.64) tam olarak doubledeğerler olarak temsil edilmeyeceğini anlamak önemli olsa da , sadece yukarıda gösterilen sorunlar nedeniyle bir sorun.


10
May extend this later - out of time right now!hevesle bekliyor @Jon
Prateek

3
daha sonra bir cevaba geri döneceğimi söylediğimde topluluk benim için biraz daha az naziktir <şaka yaptığımı göstermek için bir tür hafif yürekli ifade girin> ... buna daha sonra geri döneceğim.
Grady Player

2
@ZongZhengLi: Bunu anlamak kesinlikle önemli olsa da, bu durumda temel neden bu değil. Tam olarak ikili olarak temsil edilen değerlerle benzer bir örnek yazabilir ve aynı etkiyi görebilirsiniz. Buradaki sorun büyük ölçekli bilgileri ve küçük ölçekli bilgileri aynı anda korumaktır.
Jon Skeet

1
@Buksy: 10000'e yuvarlandı - çünkü sadece 4 önemli rakam depolayabilen bir veri türü ile uğraşıyoruz. (yani x.xxx * 10 ^ n)
Jon Skeet

3
@meteors: Hayır, taşmaya neden olmaz - ve yanlış numaraları kullanıyorsunuz. 10001, 10000'e yuvarlanır, 1001 değil 1000'e yuvarlanır. "Dört anlamlı basamak" ile "maksimum 9999 değeri" arasında büyük bir fark vardır. Daha önce söylediğim gibi, temelde x.xxx * 10 ^ n'yi temsil ediyorsunuz, burada 10000 için x.xxx 1.000 ve n 4 olacaktır. Bu, tıpkı doubleve floatçok büyük sayılar için ardışık temsil edilebilir sayılar gibi 1'den fazla apart.
Jon Skeet

52

İşte ikili olarak neler oluyor. Bildiğimiz gibi, bazı kayan nokta değerleri tam olarak ondalık olarak gösterilse bile tam olarak ikili olarak temsil edilemez. Bu 3 sayı sadece bu gerçeğin örnekleridir.

Bu program ile her bir sayının onaltılık gösterimlerini ve her bir eklemenin sonuçlarını çıktılarım.

public class Main{
   public static void main(String args[]) {
      double x = 23.53;   // Inexact representation
      double y = 5.88;    // Inexact representation
      double z = 17.64;   // Inexact representation
      double s = 47.05;   // What math tells us the sum should be; still inexact

      printValueAndInHex(x);
      printValueAndInHex(y);
      printValueAndInHex(z);
      printValueAndInHex(s);

      System.out.println("--------");

      double t1 = x + y;
      printValueAndInHex(t1);
      t1 = t1 + z;
      printValueAndInHex(t1);

      System.out.println("--------");

      double t2 = x + z;
      printValueAndInHex(t2);
      t2 = t2 + y;
      printValueAndInHex(t2);
   }

   private static void printValueAndInHex(double d)
   {
      System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
   }
}

printValueAndInHexYöntem sadece bir onaltılık-yazıcı yardımcısıdır.

Çıktı aşağıdaki gibidir:

403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004

İlk 4 sayılardır x, y, zve s'nin onaltılık gösterimleri. IEEE kayan nokta gösteriminde, 2-12 bitleri ikili üssü , yani sayının ölçeğini temsil eder. (İlk bit, işaret biti ve mantis için kalan bitlerdir .) Temsil edilen üs aslında ikili sayı eksi 1023'tür.

İlk 4 sayının üsleri çıkarılır:

    sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5

İlk toplama grubu

İkinci sayı ( y) daha küçük boyuttadır. Almak için bu iki sayıyı eklerken x + y, ikinci sayının ( 01) son 2 biti aralık dışına kaydırılır ve hesaplamaya dahil edilmez.

İkinci bir ek ekler x + yve zaynı ölçek iki sayı ekler.

İkinci toplama grubu

Burada, x + zönce gerçekleşir. Aynı ölçeğe sahiptirler, ancak ölçeğinde daha yüksek bir sayı verir:

404 => 0|100 0000 0100| => 1028 - 1023 = 5

İkinci toplama eklenir x + zve yşimdi sayılar eklemek için 3 bit çıkarılır y( 101). Burada, yukarı doğru bir yuvarlak olmalıdır, çünkü sonuç bir sonraki kayan nokta sayısıdır: 4047866666666666ilk toplama grubu 4047866666666667için, ikinci toplama seti için. Bu hata toplamın çıktısında gösterilecek kadar önemlidir.

Sonuç olarak, IEEE sayıları üzerinde matematiksel işlemler yaparken dikkatli olun. Bazı temsiller kesin değildir ve ölçekler farklı olduğunda daha da kesinleşmezler. Mümkünse benzer ölçekte numaralar ekleyin ve çıkarın.


Ölçeklerin farklı olması önemlidir. İkili olarak temsil edilen değerleri tam olarak girdilerle (ondalık olarak) yazabilir ve yine de aynı soruna sahip olabilirsiniz.
Jon Skeet

@rgettman Bir programcı olarak, =)onaltılı yazıcı yardımcısı için cevabınızı daha iyi +1 beğendim ... Bu gerçekten temiz!
ADTC

44

Jon'un cevabı elbette doğrudur. Sizin durumunuzda, hata herhangi bir basit kayan nokta işlemi yaparken biriktireceğiniz hatadan daha büyük değildir. Bir durumda sıfır hata, diğerinde küçük bir hata aldığınız bir senaryo var; bu aslında ilginç bir senaryo değil. İyi bir soru şudur: hesaplamaların sırasını değiştirmenin küçük bir hatadan (göreceli) büyük bir hataya gittiği senaryolar var mı? Cevap açık bir şekilde evet.

Örneğin şunu düşünün:

x1 = (a - b) + (c - d) + (e - f) + (g - h);

vs

x2 = (a + c + e + g) - (b + d + f + h);

vs

x3 = a - b + c - d + e - f + g - h;

Açıkçası tam aritmetik olarak aynı olurdu. A, b, c, d, e, f, g, h için değerleri bulmaya çalışmak eğlencelidir, böylece x1 ve x2 ve x3 değerleri büyük miktarda farklılık gösterir. Bakalım yapabilir misin?


Büyük bir miktarı nasıl tanımlarsınız? 1000'inci sıradan mı bahsediyoruz? 100ths? 1'ler ???
Cruncher

3
@Cruncher: Tam matematiksel sonucu ve x1 ve x2 değerlerini hesaplayın. Gerçek ve hesaplanan sonuçlar e1 ve e2 arasındaki tam matematiksel farkı çağırın. Artık hata boyutunu düşünmenin birkaç yolu var. Birincisi: bir senaryo bulabilir misiniz | e1 / e2 | veya | e2 / e1 | büyüktür? Mesela, birinin hatasını diğerinin on katına çıkarabilir misiniz? Daha ilginç olanı, bir hatayı doğru cevabın büyüklüğünün önemli bir kısmı haline getirebilirseniz.
Eric Lippert

1
Çalışma zamanı hakkında konuştuğunu anlıyorum, ama merak ediyorum: İfade derleme zamanı (diyelim constexpr) bir ifadeyse, derleyiciler hatayı en aza indirecek kadar akıllı mıdır?
Kevin Hsu

@kevinhsu genel olarak hayır, derleyici o kadar akıllı değil. Tabii ki derleyici işlemi seçmişse kesin aritmetik olarak yapmayı seçebilir, ancak genellikle yapmaz.
Eric Lippert

8
@frozenkoi: Evet, hata çok kolay sonsuz olabilir. Örneğin, C #'ı düşünün: double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);- Çıktı Infinity sonra 0'dır.
Jon Skeet

10

Bu aslında sadece Java ve Javascript'ten çok daha fazlasını kapsar ve muhtemelen float veya double kullanarak herhangi bir programlama dilini etkiler.

Bellekte, kayan noktalar IEEE 754 çizgileri boyunca özel bir format kullanır (dönüştürücü benden daha iyi bir açıklama sağlar).

Her neyse, şamandıra dönüştürücü.

http://www.h-schmidt.net/FloatConverter/

Operasyonların sırası ile ilgili olan şey operasyonun "inceliği" dir.

İlk satırınız ilk iki değerden 29,41 değerini verir, bu da üs olarak bize 2 ^ 4 verir.

İkinci satırınız 41.17 değerini verir ve bu da üs olarak bize 2 ^ 5 verir.

Sonucu değiştirmesi muhtemel üssü artırarak önemli bir rakam kaybediyoruz.

41.17 için en sağdaki son biti işaretleyip açmayı deneyin ve üssün 1/2 ^ 23'ü kadar "önemsiz" bir şeyin bu kayan nokta farkına neden olması için yeterli olacağını görebilirsiniz.

Düzenleme: Önemli rakamları hatırlayanlar için, bu kategoriye girer. 10 ^ 4 + 4999 anlamlı bir rakamla 10 ^ 4 olacaktır. Bu durumda, önemli rakam çok daha küçüktür, ancak .00000000004 eklenmiş sonuçları görebiliriz.


9

Kayan nokta sayıları, mantis (anlamlı) için belirli bir bit boyutu sağlayan IEEE 754 formatı kullanılarak temsil edilir. Maalesef bu size oynamak için belirli sayıda 'kesirli yapı taşı' verir ve belirli kesirli değerler tam olarak temsil edilemez.

Sizin durumunuzda olan şey, ikinci durumda, eklemelerin değerlendirilme sırası nedeniyle ekleme işleminin muhtemelen bir miktar hassas sorunla karşılaşmasıdır. Değerleri hesaplamamış olabilirim, ancak 23,53 + 17,64 tam olarak temsil edilemezken, 23,53 + 5,88 olabilir.

Ne yazık ki, sadece uğraşmanız gereken bilinen bir sorundur.


6

İnanıyorum ki bu değerlendirme sırası ile ilgili. Bir matematik dünyasında toplam doğal olarak aynı olsa da, ikili dünyada A + B + C = D yerine,

A + B = E
E + C = D(1)

Yani kayan nokta sayılarının kalkabileceği ikincil adım var.

Siparişi değiştirdiğinizde,

A + C = F
F + B = D(2)

4
Bu cevabın gerçek sebebi önlediğini düşünüyorum. "kayan nokta sayılarının kalkabileceği ikincil adım var". Açıkçası, bu doğru, ama açıklamak istediğimiz şey neden .
Zong
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.