Kiedy powinienem używać podwójnego zamiast dziesiętnego?


265

Mogę wymienić trzy zalety używania double(lub float) zamiast decimal:

  1. Zużywa mniej pamięci.
  2. Szybsze, ponieważ operacje matematyczne zmiennoprzecinkowe są natywnie obsługiwane przez procesory.
  3. Może reprezentować większy zakres liczb.

Ale te zalety wydają się mieć zastosowanie tylko do operacji intensywnie obliczających, takich jak te występujące w oprogramowaniu do modelowania. Oczywiście podwójnych nie należy stosować, gdy wymagana jest precyzja, na przykład obliczenia finansowe. Czy są więc jakieś praktyczne powody, aby kiedykolwiek wybierać double(lub float) zamiast decimalw „normalnych” aplikacjach?

Zredagowano, aby dodać: Dzięki za wszystkie świetne odpowiedzi, nauczyłem się od nich.

Kolejne pytanie: kilka osób stwierdziło, że liczby podwójne mogą dokładniej przedstawiać liczby rzeczywiste. Gdy zostaną zadeklarowane, sądzę, że zwykle również bardziej dokładnie je przedstawiają. Ale czy to prawda, że ​​dokładność może się zmniejszyć (czasem znacznie) podczas wykonywania operacji zmiennoprzecinkowych?



5
Jest to dość regularnie oceniane i wciąż mam z tym problem. Na przykład pracuję nad aplikacją, która wykonuje obliczenia finansowe, więc używam dziesiętnych w całym. Ale funkcje matematyczne i VisualBasic.Finansowe używają podwójnie, więc jest wiele konwersji, co sprawia, że ​​ciągle zgaduję użycie dziesiętnego.
Jamie Ide

@JamieIde to szalone, że funkcje finansowe używają podwójnie, pieniądze zawsze powinny być dziesiętne.
Chris Marisic

@ChrisMarisic Ale co Jamie Ide może zrobić ze starszym badziewiem przy użyciu podwójnego? W takim razie powinieneś użyć podwójnie, ponieważ wiele konwersji spowoduje błędy zaokrąglania ... nic dziwnego, że wspominał o VisualBasic pfffhh .....
Elisabeth

@Elisabeth prawdopodobnie użyłbym innej biblioteki, która poprawnie obsługuje dziesiętne. Wszystko, co zapewnia VisualBasic.Financial, istnieje obecnie w wielu innych bibliotekach
Chris Marisic

Odpowiedzi:


306

Myślę, że całkiem dobrze podsumowałeś zalety. Brakuje Ci jednak jednego punktu. Ten decimaltyp jest bardziej dokładny w reprezentowaniu liczb podstawowych 10 (np. Tych używanych w obliczeniach walutowych / finansowych). Ogólnie rzecz biorąc, ten doubletyp będzie oferował co najmniej tak dużą precyzję (ktoś mnie poprawi, jeśli się mylę) i zdecydowanie większą prędkość dla dowolnych liczb rzeczywistych. Prosty wniosek jest następujący: rozważając, którego użyć, zawsze używaj, doublechyba że potrzebujesz base 10dokładności, która decimaloferuje.

Edytować:

Jeśli chodzi o twoje dodatkowe pytanie dotyczące zmniejszenia dokładności liczb zmiennoprzecinkowych po operacjach, jest to nieco bardziej subtelna kwestia. Rzeczywiście, precyzja (używam terminu zamiennie dla dokładności tutaj) stopniowo spada po każdej operacji. Wynika to z dwóch powodów:

  1. fakt, że pewne liczby (oczywiście dziesiętne) nie mogą być w rzeczywistości reprezentowane w postaci zmiennoprzecinkowej
  2. pojawiają się błędy zaokrąglania, tak jakbyś robił obliczenia ręcznie. Zależy to w dużej mierze od kontekstu (liczby wykonywanych operacji), czy te błędy są na tyle znaczące, że wymagają jednak wielu przemyśleń.

We wszystkich przypadkach, jeśli chcesz porównać dwie liczby zmiennoprzecinkowe, które teoretycznie powinny być równoważne (ale zostały osiągnięte przy użyciu różnych obliczeń), musisz pozwolić na pewien stopień tolerancji (jak bardzo się różni, ale zazwyczaj jest bardzo mały) .

Aby uzyskać bardziej szczegółowy przegląd konkretnych przypadków, w których można wprowadzić błędy w dokładności, zobacz sekcję Dokładność w artykule w Wikipedii . Na koniec, jeśli chcesz poważnie dogłębnej (i matematycznej) dyskusji na temat liczb zmiennoprzecinkowych / operacji na poziomie maszyny, spróbuj przeczytać często cytowany artykuł Co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej .


1
Czy możesz podać przykład liczby podstawowej 10, z którą traci się precyzję podczas konwersji na bazę 2?
Mark Cidade,

@Mark: 1.000001 jest jednym przykładem, przynajmniej według Jona Skeeta. (Patrz pytanie 3 tej strony: yoda.arachsys.com/csharp/teasers-answers.html )
Noldorin

25
@ Mark: bardzo prosty przykład: 0,1 jest ułamkiem okresowym w bazie 2, więc nie można go dokładnie wyrazić w a double. Współczesne komputery nadal będą drukować poprawną wartość, ale tylko dlatego, że „zgadują” wynik - nie dlatego, że tak naprawdę jest poprawnie wyrażona.
Konrad Rudolph

1
Ten Decimaltyp ma 93-bitową precyzję w mantysie, w porównaniu z około 52 dla double. Chciałbym jednak, żeby Microsoft wspierał 80-bitowy format IEEE, nawet jeśli musiałby zostać uzupełniony do 16 bajtów; pozwoliłby na większy zasięg doublelub Decimal, znacznie lepszą prędkość Decimal, wsparcie dla operacji transcendentalnych (np. sin (x), log (x) itd.) i precyzję, która, choć nie tak dobra, jak Decimalbyłaby znacznie lepsza niż double.
supercat

@charlotte: Jeśli przeczytasz mój pełny post, zobaczysz, że to wyjaśniono.
Noldorin,

59

Wydaje ci się, że masz zalety korzystania z typu zmiennoprzecinkowego. Zwykle projektuję dziesiętne we wszystkich przypadkach i polegam na profilerze, który informuje mnie, czy operacje dziesiętne powodują wąskie gardła lub spowolnienia. W takich przypadkach „rzucę” w dół, aby podwajać lub unosić, ale robię to tylko wewnętrznie i ostrożnie staram się zarządzać utratą precyzji, ograniczając liczbę znaczących cyfr w wykonywanej operacji matematycznej.

Ogólnie rzecz biorąc, jeśli twoja wartość jest przejściowa (nie jest ponownie wykorzystywana), możesz bezpiecznie używać typu zmiennoprzecinkowego. Rzeczywistym problemem związanym z typami zmiennoprzecinkowymi są następujące trzy scenariusze.

  1. Agregujesz wartości zmiennoprzecinkowe (w takim przypadku złożona liczba błędów precyzji)
  2. Budujesz wartości na podstawie wartości zmiennoprzecinkowych (na przykład w algorytmie rekurencyjnym)
  3. Robisz matematykę z bardzo dużą liczbą cyfr znaczących (na przykład 123456789.1 * .000000000000000987654321)

EDYTOWAĆ

Zgodnie z dokumentacją referencyjną dotyczącą liczb dziesiętnych w C # :

Dziesiętny kluczowe oznacza typ danych 128-bitowym. W porównaniu do typów zmiennoprzecinkowych, typ dziesiętny ma większą precyzję i mniejszy zakres, co czyni go odpowiednim do obliczeń finansowych i pieniężnych.

Aby wyjaśnić moje powyższe stwierdzenie:

Zwykle projektuję dziesiętne we wszystkich przypadkach i polegam na profilerze, który informuje mnie, czy operacje dziesiętne powodują wąskie gardła lub spowolnienia.

Pracowałem tylko w branżach, w których dziesiętne są korzystne. Jeśli pracujesz nad silnikami fizyki lub graficznymi, prawdopodobnie bardziej korzystne jest zaprojektowanie typu zmiennoprzecinkowego (zmiennoprzecinkowego lub podwójnego).

Dziesiętny nie jest nieskończenie dokładny (nie można przedstawić nieskończonej precyzji dla niecałkowitej w prymitywnym typie danych), ale jest znacznie bardziej precyzyjny niż dwukrotność:

  • dziesiętny = 28-29 cyfr znaczących
  • double = 15-16 cyfr znaczących
  • liczba zmiennoprzecinkowa = 7 cyfr znaczących

EDYCJA 2

W odpowiedzi na komentarz Konrada Rudolfa punkt 1 (powyżej) jest zdecydowanie poprawny. Agregacja niedokładności rzeczywiście się pogarsza. Zobacz poniższy kod dla przykładu:

private const float THREE_FIFTHS = 3f / 5f;
private const int ONE_MILLION = 1000000;

public static void Main(string[] args)
{
    Console.WriteLine("Three Fifths: {0}", THREE_FIFTHS.ToString("F10"));
    float asSingle = 0f;
    double asDouble = 0d;
    decimal asDecimal = 0M;

    for (int i = 0; i < ONE_MILLION; i++)
    {
        asSingle += THREE_FIFTHS;
        asDouble += THREE_FIFTHS;
        asDecimal += (decimal) THREE_FIFTHS;
    }
    Console.WriteLine("Six Hundred Thousand: {0:F10}", THREE_FIFTHS * ONE_MILLION);
    Console.WriteLine("Single: {0}", asSingle.ToString("F10"));
    Console.WriteLine("Double: {0}", asDouble.ToString("F10"));
    Console.WriteLine("Decimal: {0}", asDecimal.ToString("F10"));
    Console.ReadLine();
}

To powoduje, że:

Three Fifths: 0.6000000000
Six Hundred Thousand: 600000.0000000000
Single: 599093.4000000000
Double: 599999.9999886850
Decimal: 600000.0000000000

Jak widać, mimo że dodajemy z tej samej stałej źródłowej, wyniki podwójności są mniej precyzyjne (chociaż prawdopodobnie będą się zaokrąglały poprawnie), a liczba zmiennoprzecinkowa jest znacznie mniej dokładna, do tego stopnia, że ​​została zredukowana do tylko dwie cyfry znaczące.


1
Punkt 1 jest niepoprawny. Błędy precyzji / zaokrąglania występują tylko podczas odlewania, a nie w obliczeniach. Jest to oczywiście prawdą, że większość operacji matematycznych są nietrwałe, zatem mnożąc błąd. Ale to kolejna kwestia i dotyczy to wszystkich typów danych o ograniczonej precyzji, a zwłaszcza dziesiętnych.
Konrad Rudolph

1
@Konrad Rudolph, patrz przykład w „EDYCJI 2” jako dowód na punkt, który próbowałem zrobić w punkcie 1. Często ten problem nie objawia się, ponieważ niedokładność dodatnia równoważy się z niedokładnością ujemną i zmywają się agregacja, ale agregacja tej samej liczby (jak w przykładzie) uwidacznia problem.
Michael Meadows

Świetny przykład. Właśnie pokazałem to moim młodszym programistom, dzieci były zaskoczone.
Machado,

Teraz możesz zrobić to samo z 2 / 3rds zamiast 3 / 5th ... Powinieneś dowiedzieć się o systemie liczb szesnastkowych, który doskonale radzi sobie z 2/3rds.
gnasher729

1
@ gnasher729, używanie 2 / 3rds zamiast 3 / 5th nie było obsługiwane idealnie dla różnych typów. Co ciekawe, uzyskano wartość zmiennoprzecinkową, Single: 667660.400000000000a uzyskano wartość dziesiętną Decimal: 666666.7000000000. Wartość zmiennoprzecinkowa jest nieco mniejsza niż tysiąc ponad prawidłową wartość.
jhenninger

25

Użyj wartości dziesiętnych dla 10 podstawowych wartości, np. Obliczeń finansowych, jak sugerowali inni.

Ale podwójna jest generalnie dokładniejsza dla dowolnych obliczonych wartości.

Na przykład, jeśli chcesz obliczyć wagę każdej linii w portfelu, użyj podwójnie, ponieważ wynik będzie prawie równy 100%.

W poniższym przykładzie doubleResult jest bliższy 1 niż decimalResult:

// Add one third + one third + one third with decimal
decimal decimalValue = 1M / 3M;
decimal decimalResult = decimalValue + decimalValue + decimalValue;
// Add one third + one third + one third with double
double doubleValue = 1D / 3D;
double doubleResult = doubleValue + doubleValue + doubleValue;

Ponownie na przykładzie portfela:

  • Wartość rynkowa każdej linii w portfelu jest wartością pieniężną i prawdopodobnie najlepiej byłaby reprezentowana jako dziesiętna.

  • Waga każdej linii w portfelu (= wartość rynkowa / SUMA (wartość rynkowa)) jest zwykle lepiej reprezentowana jako podwójna.


6

Użyj podwójnego lub zmiennoprzecinkowego, gdy nie potrzebujesz precyzji, na przykład w napisanej przeze mnie platformówce użyłem zmiennoprzecinkowego do przechowywania prędkości gracza. Oczywiście nie potrzebuję tutaj super precyzji, ponieważ w końcu wybieram Int do rysowania na ekranie.


3
Precyzja jest jedyną zaletą miejsc po przecinku, to prawda. Nie powinieneś pytać, kiedy powinieneś używać liczb zmiennoprzecinkowych na liczbach dziesiętnych. To powinna być twoja pierwsza myśl. Pytanie brzmi zatem, kiedy należy używać miejsc po przecinku (a odpowiedź jest właśnie tutaj ... kiedy liczy się precyzja).
Instance Hunter

3
@Daniel Straight, To zabawne, ale mam przeciwną opinię. Myślę, że użycie mniej precyzyjnego typu ze względu na jego charakterystykę wydajności stanowi wstępną optymalizację. Być może będziesz musiał wielokrotnie zapłacić za tę wstępną optymalizację, zanim zdasz sobie sprawę z jej korzyści.
Michael Meadows

3
@Michael Meadows, rozumiem ten argument. Należy jednak zauważyć, że jednym z głównych zarzutów związanych z przedwczesną optymalizacją jest to, że programiści nie wiedzą, co będzie wolne. Wiemy jednak bez wątpienia, że ​​ułamki dziesiętne są wolniejsze niż podwójne. Niemniej jednak przypuszczam, że w większości przypadków poprawa wydajności i tak nie będzie zauważalna dla użytkownika. Oczywiście w większości przypadków precyzja też nie jest potrzebna. Heh
Instance Hunter

Dziesiętny zmiennoprzecinkowy jest w rzeczywistości MNIEJSZY precyzyjny niż binarny zmiennoprzecinkowy przy użyciu tej samej liczby bitów. Zaletą Decimal jest możliwość dokładnego przedstawienia ułamków dziesiętnych, takich jak 0,01, które są wspólne w obliczeniach finansowych.
dan04

Cóż, nie jest to całkiem poprawne :) - w wielu grach liczby zmiennoprzecinkowe mogą być niepożądane, ponieważ nie są spójne. Zobacz tutaj
BlueRaja - Danny Pflughoeft

4

W niektórych rachunkach rozważ możliwość zastosowania typów integralnych zamiast lub w połączeniu. Na przykład, powiedzmy, że zasady, którymi się kierujesz, wymagają, aby każdy wynik obliczeń był przenoszony co najmniej o 6 miejsc po przecinku, a wynik końcowy zostanie zaokrąglony do najbliższego centa.

Obliczenie 1/6 ze 100 $ daje 16,666666666666666 ... więc wartość przeprowadzona w arkuszu wyniesie 16,6666667. Zarówno podwójne, jak i dziesiętne powinny dać wynik dokładnie do 6 miejsc po przecinku. Możemy jednak uniknąć błędu skumulowanego, przenosząc wynik jako liczbę całkowitą 16666667. Każde kolejne obliczenie można wykonać z tą samą precyzją i przenieść w podobny sposób. Kontynuując przykład, obliczam podatek od sprzedaży w Teksasie od tej kwoty (16666667 * .0825 = 1375000). Dodanie dwóch (jest to krótki arkusz) 1666667 + 1375000 = 18041667. Przesunięcie punktu dziesiętnego z powrotem daje nam 18.041667, czyli 18,04 USD.

Chociaż ten krótki przykład nie dałby skumulowanego błędu przy użyciu podwójnej lub dziesiętnej, dość łatwo jest pokazać przypadki, w których zwykłe obliczenie podwójnej lub dziesiętnej i przeniesienie spowodowałoby znaczny błąd. Jeśli reguły, na których działasz, wymagają ograniczonej liczby miejsc po przecinku, przechowywanie każdej wartości jako liczby całkowitej przez pomnożenie przez 10 ^ (wymagany # miejsca po przecinku), a następnie podzielenie przez 10 ^ (wymagany # miejsc po przecinku), aby uzyskać rzeczywistą wartość pozwoli uniknąć błędu skumulowanego.

W sytuacjach, w których nie występują ułamki grosza (na przykład automat), nie ma powodu, aby w ogóle używać typów niezintegrowanych. Pomyśl o tym jak o liczeniu groszy, a nie o dolarach. Widziałem kod, w którym każde obliczenie obejmowało tylko całe grosze, ale użycie podwójnego doprowadziło do błędów! Matematyka tylko liczby całkowite usunęła problem. Tak więc moja niekonwencjonalna odpowiedź brzmi, gdy jest to możliwe, rezygnuje zarówno z podwójnej, jak i dziesiętnej.


3

Jeśli potrzebujesz interpolacji binarnej z innymi językami lub platformami, może być konieczne użycie zmiennoprzecinkowe lub podwójne, które są znormalizowane.


2

Uwaga: ten post jest oparty na informacjach o możliwościach typu dziesiętnego z http://csharpindepth.com/Articles/General/Decimal.aspx i mojej własnej interpretacji tego, co to znaczy. Zakładam, że Double jest normalną podwójną precyzją IEEE.

Uwaga 2: najmniejsze i największe w tym poście odnoszą się do wielkości liczby.

Zalety „dziesiętnych”.

  • „dziesiętny” może reprezentować dokładnie liczby, które można zapisać jako (wystarczająco krótkie) ułamki dziesiętne, podwójne nie. Jest to ważne w księgach finansowych i podobnych, gdzie ważne jest, aby wyniki dokładnie odpowiadały temu, co dałby człowiek wykonujący obliczenia.
  • „dziesiętny” ma znacznie większą mantysę niż „podwójny”. Oznacza to, że dla wartości w ramach znormalizowanego zakresu „dziesiętny” będzie miał znacznie wyższą precyzję niż podwójna.

Wady dziesiętne

  • Będzie znacznie wolniej (nie mam testów, ale przypuszczam, że może być rzędu rzędu wielkości może więcej), dziesiętne nie skorzystają na żadnym przyspieszeniu sprzętowym, a arytmetyka na nim będzie wymagała stosunkowo drogiego mnożenia / dzielenia przez potęgi 10 ( który jest znacznie droższy niż mnożenie i dzielenie przez potęgi 2), aby dopasować wykładnik przed dodaniem / odjęciem i przywrócić wykładnik z powrotem do zakresu po pomnożeniu / dzieleniu.
  • liczba dziesiętna przepełni się wcześniej niż podwójna wola. dziesiętny może reprezentować tylko liczby do ± 2 96 -1. Dla porównania liczba podwójna może reprezentować liczby do prawie ± 2 1024
  • liczba dziesiętna spadnie wcześniej. Najmniejsze liczby reprezentowane w systemie dziesiętnym to ± 10 -28 . Dla porównania podwójne mogą reprezentować wartości do 2 -149 (około 10 -45 ), jeśli obsługiwane są liczby subnromalne, oraz 2 -126 (około 10 -38 ), jeśli nie są.
  • dziesiętny zajmuje dwa razy więcej pamięci niż podwójny.

Uważam, że powinieneś domyślnie używać „dziesiętnego” do pracy pieniężnej i innych przypadków, w których dokładne dopasowanie do ludzkich obliczeń jest ważne i że powinieneś używać podwójnego jako domyślnego wyboru przez resztę czasu.


2

Zależy od tego, czego potrzebujesz.

Ponieważ zmiennoprzecinkowe i podwójne są binarnymi typami danych, masz pewne trudności i błędy na drodze w liczbach rund, więc na przykład podwójny zaokrągliby 0,1 do 0,100000001490116, podwójny również zaokrągliby 1/3 do 0.33333334326441. Mówiąc prosto, nie wszystkie liczby rzeczywiste mają dokładne odwzorowanie w typach podwójnych

Na szczęście C # obsługuje również tak zwaną arytmetykę zmiennoprzecinkową dziesiętną, w której liczby są reprezentowane przez dziesiętny system liczbowy, a nie przez system binarny. Zatem dziesiętna arytmetyka zmiennoprzecinkowa nie traci dokładności podczas przechowywania i przetwarzania liczb zmiennoprzecinkowych. Dzięki temu doskonale nadaje się do obliczeń, w których wymagany jest wysoki poziom dokładności.


0

Użyj zmiennoprzecinkowych, jeśli cenisz wydajność nad poprawnością.


6
Liczby dziesiętne nie są bardziej poprawne, z wyjątkiem niektórych ograniczonych przypadków, które czasami (nie zawsze) są ważne.
David Thornley,

0

Wybierz typ w funkcji swojej aplikacji. Jeśli potrzebujesz precyzji jak w analizie finansowej, odpowiedziałeś na swoje pytanie. Ale jeśli twoja aplikacja może zostać rozstrzygnięta z oszacowaniem, twój ok jest podwójny.

Czy Twoja aplikacja wymaga szybkich obliczeń, czy będzie miał cały czas na świecie, aby udzielić odpowiedzi? To zależy od rodzaju aplikacji.

Głodny grafik? wystarczy float lub double. Analiza danych finansowych, meteor uderzający w planetę w pewien rodzaj precyzji? Te wymagałyby trochę precyzji :)


8
Liczby dziesiętne również są szacunkowe. Są zgodne z konwencjami arytmetyki finansowej, ale nie ma przewagi, powiedzmy, w obliczeniach dotyczących fizyki.
David Thornley,

0

Dziesiętny ma szersze bajty, podwójne jest natywnie obsługiwane przez procesor. Dziesiętny to podstawa-10, więc podczas obliczania dziesiętnego następuje konwersja dziesiętna na podwójną.

For accounting - decimal
For finance - double
For heavy computation - double

Należy pamiętać, .NET CLR obsługuje tylko Math.Pow (podwójne, podwójne). Dziesiętny nie jest obsługiwany.

.NET Framework 4

[SecuritySafeCritical]
public static extern double Pow(double x, double y);

0

Podwójne wartości będą domyślnie serializowane do notacji naukowej, jeśli notacja ta jest krótsza niż wyświetlanie dziesiętne. (np. 0,00000003 to 3e-8) Wartości dziesiętne nigdy nie będą serializowane do notacji naukowej. Może to być brane pod uwagę przy serializacji do celów konsumpcji przez podmiot zewnętrzny.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.