Dlaczego $ ((0.1)) rozwija się do 0.10000000000000001 w Zsh?


3

W zsh:

$ echo $((0.1))
0.10000000000000001

Podczas gdy w innych powłokach z zmiennoprzecinkową interpretacją arytmetyczną:

$ ksh93 -c 'echo $((0.1))'
0.1
$ yash -c 'echo $((0.1))'
0.1

Lub awk:

$ awk 'BEGIN{print 0.1 + 0}'
0.1

Dlaczego?


To kontynuacja dyskusji na czacie


1
W podwójnym zmiennoprzecinkowym, zarówno 0.1000000000000001, jak i 0.10000000000000000 mają tę samą reprezentację binarną.
Izaak

@isaac tak. W przeciwnym razie nie widzielibyśmy tego artefaktu. Zauważ, że 0.1000..1 faktycznie jest bliżej tej reprezentacji binarnej niż 0.1 i dlatego otrzymujesz ten wynik za pomocą zsh
Stéphane Chazelas

Jeśli oba mają tę samą wartość binarną, żaden nie może być bliżej. Oba są w tej samej odległości.
Izaak

2
@isaac, to wszystko w mojej odpowiedzi. 0,1 oznacza 0.00011001100110011001100110011001100110011001100110011010podwójny. To nie jest dokładnie 0,1, ponieważ nie można reprezentować 0,1 w systemie binarnym. To dokładnie 0.1000000000000000055511151231257827021181583404541015625 i .10000000000000001jest to bliższe niż 0,1, ponieważ 555 jest bliższe 1000 niż do 0. Zsh ujawnia część błędu wprowadzonego podczas konwersji na podwójną, podając 2 dodatkowe cyfry, 2 dodatkowe cyfry, które są potrzebne do przedstawienia podwójnie jednoznacznie w ogólnym przypadku (nie tym).
Stéphane Chazelas,

@ Fox Mówisz o matematyce teoretycznej (dokładne wartości), przedstawiłem rzeczywiste wyniki z rzeczywistej implementacji.
Izaak

Odpowiedzi:


8

TL; DR

zshwybiera dziesiętną reprezentację doubleliczb binarnych, której używa do oceny arytmetyki zmiennoprzecinkowej, która w pełni zachowuje ich informacje i jest bezpieczna dla ponownego wprowadzenia do wyrażeń arytmetycznych. A dzieje się to kosztem kosmetyków. Za to, że potrzebuje 17 cyfr znaczących, i upewnij się, że ekspansja zawsze zawiera .albo ewięc jest traktowane jako pływaka na reinput.

Ta „w pełni precyzyjna” reprezentacja dziesiętna może być postrzegana jako format pośredni między doubleliczbami tylko maszynowymi o precyzji binarnej a cyframi czytelnymi dla człowieka. Pośredni format rozumiany przez wszystkie narzędzia, które rozumieją dziesiętne reprezentacje liczb zmiennoprzecinkowych.

W przypadku wartości 0,1 używanej w wyrażeniu arytmetycznym zdarza się, że najbliższa 17-cyfrowa reprezentacja dziesiętna liczby podwójnej o podwójnej precyzji najbliższej 0,1 to 0,10000000000000001, artefakt spowodowany ograniczeniem precyzji liczb podwójnej precyzji i zaokrąglania.

Inne powłoki uprzywilejowują aspekt kosmetyczny i tracą część informacji po konwersji do postaci dziesiętnej (choć nadal starają się zachować jak największą precyzję w ramach tego dodatkowego ograniczenia). Oba podejścia mają swoje zalety i wady, zobacz szczegóły poniżej.

awk nie ma tego rodzaju problemów, ponieważ nie jest powłoką i nie musi stale tłumaczyć w przód iw tył między reprezentacją binarną i dziesiętną podczas manipulacji zmiennoprzecinkowymi.

podejście Zsha

zsh, Podobnie jak wiele innych języków programowania (w tym yash, ksh93) oraz wiele narzędzi stosowanych z powłoki (jak awk, printf...), które dotyczą liczb zmiennoprzecinkowych, wykonywać operacje arytmetyczne na binarnej reprezentacji tych liczb.

Jest to wygodne i wydajne, ponieważ operacje te są obsługiwane przez kompilator C, a na większości architektur są wykonywane przez sam procesor.

zshużywa doubletypu C do wewnętrznej reprezentacji liczb rzeczywistych.

W większości architektur (i większości kompilatorów) są one implementowane przy użyciu podwójnych punktów zmiennoprzecinkowych podwójnej precyzji IEEE 754.

Są one zaimplementowane trochę podobnie jak nasze liczby inżynierskie w notacji inżynierskiej 1.12e4, ale w postaci binarnej (podstawa 2) zamiast dziesiętnej (podstawa 10). Z mantysą na 53 bitach (z czego 1 implikowana) i wykładnikiem na 11 bitach (i bitem znaku). Zazwyczaj zapewniają one większą precyzję niż byś kiedykolwiek potrzebował.

Podczas oceny wyrażenia arytmetycznego typu 1. / 10(który tutaj ma literalną stałą zmiennoprzecinkową jako jednego z operandów), zshkonwertuje je z doublewewnętrznej reprezentacji dziesiętnej tekstu na s wewnętrznie (przy użyciu strtod()funkcji standardowej ) i wykonuje operację, która skutkuje nową double.

1/10 można przedstawić za pomocą zapisu dziesiętnego jako 0,1 lub 1e-1, ale tak jak nie możemy reprezentować 1/3 po przecinku (byłoby dobrze w podstawie 3, 6 lub 9), 1/10 nie może być reprezentowane binarnie (ponieważ 10 nie jest potęgą 2). Podobnie jak 1/3 to 0,33333 adlib w systemie dziesiętnym, 1/10 to .0001100110011001100110011001 adlib lub 1.10011001100110011001 adlib p-4 w systemie binarnym (gdzie p-4oznacza 2 -4 , (4 tutaj w systemie dziesiętnym)).

Ponieważ możemy przechowywać tylko 52 bity 1001..., 1/10 doublestaje się 1.1001100110011001100110011001100110011001100110011010p-4 (zwróć uwagę na zaokrąglenie ostatnich 2 cyfr).

To najbliższa reprezentacja 1/10, którą możemy uzyskać za pomocą doubles. Jeśli przekonwertujemy to z powrotem na dziesiętne, otrzymamy:

#         1         2
#12345678901234567890
.1000000000000000055511151231257827021181583404541015625

doubleWcześniej (1.1001100110011001100110011001100110011001100110011001p-4:

.09999999999999999167332731531132594682276248931884765625

i następny (1.1001100110011001100110011001100110011001100110011011p-4):

.10000000000000001942890293094023945741355419158935546875

nie są tak blisko.

Teraz zshjest przede wszystkim powłoką, to znaczy interpreterem wiersza poleceń. Wcześniej czy później będzie musiał przekazać do polecenia liczbę zmiennoprzecinkową wynikającą z wyrażenia arytmetycznego. W języku programowania innym niż shell, możesz przekazać doublefunkcję, którą chcesz wywołać. Ale w powłoce można przekazywać ciągi tylko do poleceń. Nie możesz przekazać swoich surowych bajtów, doubleponieważ mogą one bardzo dobrze zawierać NUL bajtów, a mimo to polecenia nie wiedziałyby, co z nimi zrobić.

Musisz więc przekonwertować go z powrotem na notację łańcuchową zrozumiałą dla polecenia. Istnieją pewne notacje, takie jak notacja zmiennoprzecinkowa C99 0xc.ccccccccccccccdp-7, która może z łatwością reprezentować binarną liczbę zmiennoprzecinkową IEEE 754, ale nie jest jeszcze szeroko obsługiwana i bardziej ogólnie bez znaczenia dla większości śmiertelnych ludzi (początkowo niewiele osób rozpoznaje 0,1 widok powyżej). Zatem wynikiem $((...))rozszerzenia arytmetycznego jest liczba zmiennoprzecinkowa w zapisie dziesiętnym¹.

Teraz .1000000000000000055511151231257827021181583404541015625 jest nieco długi i nie ma sensu dawać tak dużej precyzji, biorąc pod uwagę, że doubles (a więc wynik wyrażeń arytmetycznych) nie mają zbyt dużej precyzji. W efekcie .1000000000000000055511151231257827021181583404541015625, .100000000000000005551115123125782, a nawet 0,1 w tym przypadku zmieni się z powrotem na to samo double.

Jeśli skrócimy (i zaokrąglimy) do 15 cyfr, np. yash(Który również używa doubles wewnętrznie do obliczeń zmiennoprzecinkowych), otrzymamy 0,1, ale znowu otrzymamy 0,1 również dla dwóch pozostałych doubles, więc tracimy informacje, ponieważ nie możemy rozróżnić tych 3 różnych liczb. Jeśli obcinamy do 16 bitów, nadal otrzymujemy 2 z tych różnych, doublektóre dają 0,1.

Musielibyśmy zachować 17 cyfr dziesiętnych, aby nie utracić informacji przechowywanych w podwójnej precyzji IEEE 754. Jak to ujmuje artykuł z Wikipedii o podwójnej precyzji (cytując artykuł Williama Kahana, głównego architekta IEEE 754):

Jeśli liczba podwójnej precyzji IEEE 754 jest konwertowana na ciąg dziesiętny z co najmniej 17 cyframi znaczącymi, a następnie z powrotem na reprezentację podwójnej precyzji, wynik końcowy musi być zgodny z liczbą oryginalną

I odwrotnie, jeśli użyjemy mniejszej liczby bitów, istnieją doublewartości binarne , dla których nie odzyskamy tego samego doublepo przekonwertowaniu ich z powrotem, jak pokazano w powyższym przykładzie.

Tak właśnie zshjest, decyduje się zachować całą precyzję doubleformatu binarnego na reprezentację dziesiętną podaną przez wynik rozszerzenia arytmetycznego, aby po ponownym zastosowaniu do czegoś (takiego jak awklub printf "%17f"wyrażenia arytmetyczne zsh ...), który konwertuje go wraca do tego, doubleto wraca tak samo double.

Jak widać w zshkodzie (już w 2000 r., Kiedy dodano obsługę zmiennoprzecinkową zsh):

    /*
     * Conversion from a floating point expression without using
     * a variable.  The best bet in this case just seems to be
     * to use the general %g format with something like the maximum
     * double precision.
     */

Zauważysz również, że rozszerza to liczby zmiennoprzecinkowe, które okazują się nie mieć części dziesiętnej po obcięciu za pomocą .dołączonej, aby upewnić się, że są one uważane za zmiennoprzecinkowe, gdy zostaną użyte ponownie w wyrażeniu arytmetycznym:

$ zsh -c 'echo $((0.5 * 4))'
2.

Jeśli nie, i zostałby ponownie użyty w wyrażeniu arytmetycznym, byłby traktowany jako liczba całkowita zamiast liczby zmiennoprzecinkowej, co wpłynęłoby na zachowanie używanych operacji (na przykład 2/4 to dzielenie liczb całkowitych, które daje 0 i 2 ./4 jest dzielnikiem zmiennoprzecinkowym, który daje 0,5).

Teraz ten wybór liczby cyfr znaczących oznacza, że ​​w przypadku tej 0,1 jako danych wejściowych 1.1001100110011001100110011001100110011001100110011010p-4 dwójkowy double(najbliższy 0,1) staje się 0.100000000000001, co wygląda źle, gdy jest pokazane człowiekowi. Jest jeszcze gorzej, gdy błąd jest w innym kierunku, jak 0.3, który staje się 0.29999999999999999.

Istnieje również odwrotny problem, gdy przekazując tę ​​liczbę do aplikacji obsługującej większą precyzję niż doubles, faktycznie przekazujemy ten błąd 0,000000000000001 (z wartości wprowadzonej przez użytkownika, np. 0,1), po którym następnie staje się znaczący:

$ v=$((0.1)) awk 'BEGIN{print ENVIRON["v"] == 0.1}'
1
$ v=$((0.1)) yash -c 'echo "$((v == 0.1))"'
1

OK, ponieważ awki yashużywaj doubles tak jak zsh, ale:

$ echo "$((0.1)) == 0.1" | bc
0
$ v=$((0.1)) ksh93 -c 'echo "$((v == 0.1))"'
0

nie OK, ponieważ bcużywa dowolnej precyzji i ksh93rozszerzonej precyzji w moim systemie.

Teraz, jeśli zamiast 0,1 (1/10), pierwotna wartość dziesiętna wynosiła 0.11111111111111111 (lub inne dowolne przybliżenie 1/9), tabele się odwróciłyby, pokazując, że dokonywanie porównań równości na liczbach zmiennoprzecinkowych jest zupełnie beznadziejne.

Problem artefaktu wyświetlanego przez człowieka można rozwiązać, określając precyzję w momencie wyświetlania (po wykonaniu wszystkich obliczeń przy użyciu pełnej precyzji), na przykład za pomocą printf:

$ x=$((1./10)); printf '%s %g\n' $x $x
0.10000000000000001 0.1

( %g, skrót %.6god domyślnego formatu wyjściowego dla elementów zmiennoprzecinkowych awk). To również usuwa dodatkowe końcowe spacje .na liczbach całkowitych.

podejście yash (i ksh93)

yashzdecydowaliśmy się usunąć artefakty kosztem precyzji, 15 cyfr dziesiętnych to najwyższa liczba znaczących cyfr dziesiętnych, która gwarantuje, że nie będzie tego rodzaju artefaktu podczas konwersji liczby z dziesiętnej na dwójkową i z powrotem na dziesiętną, jak w naszym $((0.1))walizka.

Fakt utraty informacji w liczbie binarnej po konwersji na dziesiętną może powodować inne formy artefaktów:

$ yash -c 'x=$((1./3)); echo "$((x == 1./3)) $((1./3 == 1./3))"'
0 1

Chociaż porównania (nie) równości są na ogół niebezpieczne z zmiennoprzecinkowymi. Tutaj możemy się spodziewać xi 1./3być identycznymi, ponieważ są wynikiem dokładnie tej samej operacji.

Również:

$ yash -c  'x=$((0.5 * 3)); y=$((1.25 * 4)); echo "$((x / y))"'
0.3
$ yash -c  'x=$((0.5 * 6)); y=$((1.25 * 4)); echo "$((x / y))"'
0

(jak yash nie zawsze zawierać .lub ew reprezentacji dziesiętnym pływająca wyniku punktowej następnej operacji arytmetycznej może kończyć się albo za operacja całkowitą lub operacji zmiennoprzecinkowej).

Lub:

$ yash -c 'a=$((1e15)); echo $((a*100000))'
1e+20
$ yash -c 'a=$((1e14)); echo $((a*100000))'
-8446744073709551616

( $((1e15))rozwija się do 1e+15której przyjmuje się jako $((1e14))liczbę zmiennoprzecinkową, podczas gdy rozwija się do 100000000000000, która jest przyjmowana jako liczba całkowita i powoduje przepełnienie, ponieważ faktycznie mnożymy liczby całkowite zamiast liczb zmiennoprzecinkowych).

Chociaż istnieją sposoby rozwiązania problemów z artefaktami poprzez zmniejszenie precyzji przy wyświetlaniu, zshjak pokazano powyżej, utraty precyzji nie można odzyskać w innych powłokach.

$ yash -c 'printf "%.17g\n" $((5./9))'
0.555555555555556

(wciąż tylko 15 cyfr)

W każdym razie, bez względu na to, jak krótkie jest to obcięcie, zawsze można uzyskać artefakty w wynikach rozszerzeń arytmetycznych, ponieważ błędy są nieodłącznie związane z reprezentacjami zmiennoprzecinkowymi.

$ yash -c 'echo $((10.1 - 10))'
0.0999999999999996

Co jest kolejną ilustracją tego, dlaczego tak naprawdę nie można używać operatora równości z zmiennoprzecinkowymi:

$ zsh -c 'echo $((10.1 - 10 == 0.1))'
0
$ yash -c 'echo "$((10.1 - 10 == 0.1))"'
0

ksh93

Przypadek ksh93 jest bardziej złożony.

ksh93 używa long doubles zamiast gdy jest doubledostępny. long doubles są gwarantowane przez C tylko co najmniej tak duże jak doubles. W praktyce, w zależności od kompilatora i architektury, najczęściej są to albo podwójna precyzja IEEE 754 (64 bity), jak doubles, czterokrotna precyzja IEEE 754 (128 bitów) lub rozszerzona precyzja (80 bitów), ale często przechowywane na 128 bitach ), na przykład gdy ksh93 jest budowany dla systemów GNU / Linux działających na x86.

Aby w pełni i jednoznacznie przedstawić je w postaci dziesiętnej, potrzebujesz odpowiednio 17, 36 lub 21 cyfr znaczących.

ksh93 obcina 18 cyfr znaczących.

W tej chwili mogę testować tylko architekturę x86, ale rozumiem, że w systemach, w których long doubles są jak doubles, dostaniesz ten sam artefakt jak w przypadku zsh(gorzej, ponieważ używa 18 cyfr zamiast 17).

Tam, gdzie doubles ma 80 bitów lub 128 bitów dokładności, pojawiają się takie same problemy, jak z yashwyjątkiem tego, że sytuacja jest lepsza, gdy interakcja z narzędziami działającymi z doubles, ponieważ ksh93 daje im większą precyzję niż potrzebują i zachowałaby tyle precyzji, co oni daj to.

$ ksh93 -c 'x=$((1./3)); echo "$((x == 1. / 3))"'
0

jest nadal „problemem”, ale nie:

$ ksh93 -c 'x=$((1./3)) awk "BEGIN{print ENVIRON[\"x\"] == 1/3}"'
1

jest OK

Jednak zachowanie nie jest optymalne, kiedy typeset -F<n>/-E<n>jest używane. W takim przypadku ksh93 obcina się do 15 cyfr znaczących podczas przypisywania wartości do zmiennej, nawet jeśli żądasz wartości <n>większej niż 15:

$ ksh93 -c 'typeset -F21 x; ((x = y = 1./3)); echo "$((x == y))"'
0
$ ksh93 -c 'typeset -F21 x; ((y = 1./3)); x=$y; echo "$((x == y))"'
0

Istnieją różnice w zachowaniu pomiędzy nimi ksh93, zsha yashjeśli chodzi o obsługę znaku dziesiętnego podstawnika lokalizacji (czy użyć / rozpoznać 3.14 lub 3,14), co wpływa na zdolność do ponownego wprowadzenia wyniku rozwinięć arytmetycznych w wyrażeniach arytmetycznych. Zsh jest znowu spójny, ponieważ wynik rozszerzeń zawsze może być użyty w wyrażeniach arytmetycznych niezależnie od ustawień regionalnych użytkownika.

awk

awkjest jednym z tych języków programowania, który nie jest powłoką i obsługuje liczby zmiennoprzecinkowe. To samo dotyczyłoby perl...

Jego zmienne nie są ograniczone do łańcuchów i obecnie zwykle przechowują liczby wewnętrznie jako binarne double( gawkobsługuje także dowolne liczby precyzji jako rozszerzenie). Konwersja na notację dziesiętną ciągu ma miejsce tylko podczas drukowania liczby takiej jak w:

$ awk 'BEGIN {print 0.1}'
0.1

W takim przypadku używa formatu określonego w OFMTspecjalnej zmiennej ( %.6gdomyślnie), ale może być dowolnie duży:

$ awk -v OFMT=%.80g 'BEGIN{print 0.1}'
0.1000000000000000055511151231257827021181583404541015625

Lub gdy następuje niejawna konwersja liczby na ciąg, na przykład gdy używany jest operator ciągu (np. Konkatenacja subtr(), index()...), to w takim przypadku używana jest zmienna CONVFMT (z wyjątkiem liczb całkowitych).

$ awk -v OFMT=%.0e -v CONVFMT=%.17g 'BEGIN{x=0.1; print x, ""x}'
1e-01 0.10000000000000001

Lub przy użyciu printfjawnym.

Zwykle nie ma problemu z utratą precyzji wewnętrznie, ponieważ nie dokonujemy konwersji między reprezentacją dziesiętną a binarną. A na wyjściu można zdecydować, ile lub jak mało precyzji dać.

Wniosek

Podsumowując, przedstawię swoją osobistą opinię.

Arytmetyka zmiennoprzecinkowa powłoki nie jest czymś, czego często używam. Przez większość czasu, to przez zsh„s zcalcfunkcję kalkulatora autoloadable która drukuje pływaków z 6 cyfr precyzją tak. Przez większość czasu wszystko po pierwszych 3 cyfrach po przecinku jest po prostu hałasem dla tego rodzaju użycia.

Konieczne jest posiadanie dużej dokładności rozszerzeń arytmetycznych. Niezależnie od tego, czy jest to pełna precyzja, czy tak duża precyzja, jak to możliwe, przy jednoczesnym unikaniu niektórych artefaktów, prawdopodobnie nie ma to większego znaczenia, szczególnie biorąc pod uwagę, że nikt nigdy nie użyje powłoki do wykonywania rozległych obliczeń zmiennoprzecinkowych.

Chociaż daje mi to komfort, gdy wiem zsh, że zaokrąglanie do miejsca po przecinku nie wprowadzi dodatkowego poziomu błędów, ważniejsze jest dla mnie to, że wynik rozszerzeń można bezpiecznie stosować w wyrażeniach arytmetycznych, że zmiennoprzecinkowe pozostają zmiennoprzecinkowe i że skrypt będzie działał, gdy zostanie użyty w lokalizacji, w której ,na przykład jest podstawa dziesiętna .


¹ zsh jest jedyną powłoką podobną do Korna, o której wiem, że może mieć rozszerzenia arytmetyczne w podstawach innych niż 10, ale dotyczy to tylko liczb całkowitych.


Cześć @isaac. Jaka część odpowiedzi dotyczy sprzeciwu? Używanie rozszerzonej precyzji do wykonywania operacji arytmetycznych na podwójnych procesorach x86 (dawniej koprocesor) robi IIRC, ale jest to oddzielne od reprezentacji podwójnych na 64 bitach z 53-bitową mantysą.
Stéphane Chazelas,

Pomyśl o tym, co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej : Jedna motywacja do zwiększenia precyzji pochodzi z kalkulatorów, które często wyświetlają 10 cyfr, ale używają 13 cyfr wewnętrznie. Wyświetlając tylko 10 z 13 cyfr, kalkulator pojawia się dla użytkownika jako „czarna skrzynka”, która oblicza wykładnicze, cosinus itp. Z dokładnością do 10 cyfr.
Izaak

W skrócie, jeśli zaczniesz z szeregu (0.1), po okrągłym podróży do binarny iz powrotem, to musi skończyć z tym samym numerem (0,1). To złota zasada.
Izaak

1
@isaac (ciąg dalszy). Zastanówmy się teraz nad inną „złotą” zasadą: gdy przechowujesz wynik obliczenia wyrażenia arytmetycznego w zmiennej i że później użyjesz tej zmiennej w innym wyrażeniu arytmetycznym, ten sam wynik musi zostać użyty. Ta reguła jest niezgodna z twoją, ponieważ wynik jest przechowywany jako liczba dziesiętna, ponieważ wymaga co najmniej 17 cyfr znaczących, podczas gdy twoja reguła wymaga obcięcia liczb do 15 cyfr znaczących. Różne muszle spełniają różne zasady. Jak powiedziałem, oboje mają swoje zalety i wady.
Stéphane Chazelas

1
@isaac 17 cyfr dziesiętnych wcale nie jest arbitralny. Rozważ dwie kolejne doublewartości 3fc0000000000000i 3fc0000000000001. Aby je rozróżnić, potrzebujesz 17 cyfr dziesiętnych, odpowiednio 0,12500000000000000 i 0,12500000000000003. Ponieważ powłoka wyprowadza reprezentację dziesiętną (zarówno dla ciebie, jak i dla innych programów), aby zachować odrębne wartości, konieczne jest użycie tego 17-cyfrowego rozszerzenia
Fox

2

Binarna reprezentacja obu

0.10000000000000001
0.10000000000000000

w podwójnej jest to samo.
Skompiluj ten kod (float.c):

#include <stdlib.h>
#include <stdio.h>
void printd(double d) {
unsigned char *p = (unsigned char*) &d;
for (int i = 0; i < sizeof(d); i++)
    printf("%02x", p[i]);
printf(" %a %.80g\n", d, d);
}

int main(int argc, char *argv[]) {

double x = 0.10000000000000000;
double y = 0.10000000000000001;
if (argc == 2 ) y = strtod(argv[1], 0);
printd(x);
printd(y);
return 0;
}

Połącz z:

gcc -o flo float.c

Uruchom, aby uzyskać:

$ ./flo
9a9999999999b93f 0x1.999999999999ap-4 0.1000000000000000055511151231257827021181583404541015625
9a9999999999b93f 0x1.999999999999ap-4 0.1000000000000000055511151231257827021181583404541015625

Dokładnie taki sam plik binarny 9a9999999999b93fdla obu.

Nie ma powodu, dla którego należy wybierać jednego z drugiego z punktu widzenia samej reprezentacji binarnej.


Wartości są rzeczywiście różne tak długo podwójna (prawdopodobnie zaimplementowana jako liczba zmiennoprzecinkowa 80-bitowa):

Skompiluj ten kod:

#include <stdlib.h> 
#include <stdio.h> 
void printd(long double d) { 
unsigned char *p = (unsigned char*) &d; 
for (int i = 0; i < sizeof(d); i++) 
    printf("%02x", p[i]); 
printf(" %La %.80Lg\n", d, d); 
} 

int main(int argc, char *argv[]) {

long double x = 0.10000000000000000L;
long double y = 0.10000000000000001L;
if (argc == 2 ) y = strtold(argv[1], 0);
printd(x);
printd(y);
return 0;
}

I dostać:

$ gcc -o flod floatd.c
$ ./flod
cdccccccccccccccfb3f93ccc5550000 0xc.ccccccccccccccdp-7 0.1000000000000000000013552527156068805425093160010874271392822265625
91d2ccccccccccccfb3f000000000000 0xc.cccccccccccd291p-7 0.1000000000000000100031202938943852842612614040262997150421142578125

Różnica polega na tym, w jakim momencie zaokrąglenie zostało wykonane. Zaokrąglanie w górę dzostało wykonane w ostatnim bicie w 0.10000000000000000L (64 bit 0xc.ccccccccccccccdp-7):

0xc.ccccccccccccccdp-7

Ale został wykonany na 52-tym:

0xc.cccccccccccd291p-7

Długość 52 bitów to zwykle długość mantysy z podwójnym pływakiem. Oznacza to, że wartość jest zgodna z 80-bitową liczbą zmiennoprzecinkową zaokrągloną jako liczba zmiennoprzecinkowa.


Powodem do wyboru 0.10000000000000001ponad 0.1gdy używasz 17 cyfr znaczących jest bo 0.10000000000000001jest bliżej 0.1000000000000000055511151231257827021181583404541015625niż 0.1jest. To właśnie printf("%.17g")poprawnie daje.
Stéphane Chazelas,

0.10000000000000001 wynika z arbitralnego wyboru zsh z printf %.17g "$((0.1))".
Izaak

Proszę przeczytać moją odpowiedź, aby zrozumieć, dlaczego powłoka chciałaby użyć 17 cyfr dziesiętnych precyzji do przedstawienia wyniku wyrażeń arytmetycznych. Przestanę próbować wyjaśniać. Spędziłem już na to zdecydowanie za dużo czasu.
Stéphane Chazelas

0

Krótka odpowiedź brzmi: 1/10 nie jest prostym ułamkiem w bazie 2 i nie może być reprezentowana przez skończoną liczbę cyfr 2 bazy.

zsh oczywiście używa wewnętrznej reprezentacji danych zmiennoprzecinkowych do oceny wyrażeń zmiennoprzecinkowych i formatowania konwersji.


To bardzo prawda, ale tak naprawdę nie tłumaczy różnicy w stosunku do innych powłok i awk, które również używają wewnętrznej binarnej reprezentacji danych zmiennoprzecinkowych. W efekcie główną różnicą jest liczba cyfr znaczących, których używają w reprezentacji dziesiętnej i dokładny typ zmiennoprzecinkowy (15 cyfr w yash, 17 w zsh, 18 w ksh93, ale ksh93 używa długich podwójnych zamiast podwójnych)
Stéphane Chazelas
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.