TL; DR
zsh
wybiera dziesiętną reprezentację double
liczb 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 e
wię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 double
liczbami 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.
zsh
używa double
typu 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), zsh
konwertuje je z double
wewnę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-4
oznacza 2 -4 , (4 tutaj w systemie dziesiętnym)).
Ponieważ możemy przechowywać tylko 52 bity 1001...
, 1/10 double
staje 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ą double
s. Jeśli przekonwertujemy to z powrotem na dziesiętne, otrzymamy:
# 1 2
#12345678901234567890
.1000000000000000055511151231257827021181583404541015625
double
Wcześniej (1.1001100110011001100110011001100110011001100110011001p-4:
.09999999999999999167332731531132594682276248931884765625
i następny (1.1001100110011001100110011001100110011001100110011011p-4):
.10000000000000001942890293094023945741355419158935546875
nie są tak blisko.
Teraz zsh
jest 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ć double
funkcję, 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, double
ponieważ 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 double
s (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 double
s wewnętrznie do obliczeń zmiennoprzecinkowych), otrzymamy 0,1, ale znowu otrzymamy 0,1 również dla dwóch pozostałych double
s, 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, double
któ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ą double
wartości binarne , dla których nie odzyskamy tego samego double
po przekonwertowaniu ich z powrotem, jak pokazano w powyższym przykładzie.
Tak właśnie zsh
jest, decyduje się zachować całą precyzję double
formatu binarnego na reprezentację dziesiętną podaną przez wynik rozszerzenia arytmetycznego, aby po ponownym zastosowaniu do czegoś (takiego jak awk
lub printf "%17f"
wyrażenia arytmetyczne zsh ...), który konwertuje go wraca do tego, double
to wraca tak samo double
.
Jak widać w zsh
kodzie (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ż double
s, 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ż awk
i yash
używaj double
s tak jak zsh
, ale:
$ echo "$((0.1)) == 0.1" | bc
0
$ v=$((0.1)) ksh93 -c 'echo "$((v == 0.1))"'
0
nie OK, ponieważ bc
używa dowolnej precyzji i ksh93
rozszerzonej 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 %.6g
od 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)
yash
zdecydowaliś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ć x
i 1./3
być 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 e
w 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+15
któ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, zsh
jak 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 double
s zamiast gdy jest double
dostępny. long double
s są gwarantowane przez C tylko co najmniej tak duże jak double
s. W praktyce, w zależności od kompilatora i architektury, najczęściej są to albo podwójna precyzja IEEE 754 (64 bity), jak double
s, 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 double
s są jak double
s, dostaniesz ten sam artefakt jak w przypadku zsh
(gorzej, ponieważ używa 18 cyfr zamiast 17).
Tam, gdzie double
s ma 80 bitów lub 128 bitów dokładności, pojawiają się takie same problemy, jak z yash
wyjątkiem tego, że sytuacja jest lepsza, gdy interakcja z narzędziami działającymi z double
s, 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
, zsh
a yash
jeś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
awk
jest 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
( gawk
obsł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 OFMT
specjalnej zmiennej ( %.6g
domyś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 printf
jawnym.
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 zcalc
funkcję 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.