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.