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?
zsh
arithmetic
floating-point
Stéphane Chazelas
źródło
źródło
0.00011001100110011001100110011001100110011001100110011010
podwójny. To nie jest dokładnie 0,1, ponieważ nie można reprezentować 0,1 w systemie binarnym. To dokładnie 0.1000000000000000055511151231257827021181583404541015625 i.10000000000000001
jest 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).Odpowiedzi:
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.
alboe
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 tymyash
,ksh93
) oraz wiele narzędzi stosowanych z powłoki (jakawk
,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żywadouble
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 zdouble
wewnętrznej reprezentacji dziesiętnej tekstu na s wewnętrznie (przy użyciustrtod()
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/10double
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:double
Wcześniej (1.1001100110011001100110011001100110011001100110011001p-4:i następny (1.1001100110011001100110011001100110011001100110011011p-4):
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 samodouble
.Jeśli skrócimy (i zaokrąglimy) do 15 cyfr, np.
yash
(Który również używadouble
s wewnętrznie do obliczeń zmiennoprzecinkowych), otrzymamy 0,1, ale znowu otrzymamy 0,1 również dla dwóch pozostałychdouble
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):
I odwrotnie, jeśli użyjemy mniejszej liczby bitów, istnieją
double
wartości binarne , dla których nie odzyskamy tego samegodouble
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 jakawk
lubprintf "%17f"
wyrażenia arytmetyczne zsh ...), który konwertuje go wraca do tego,double
to wraca tak samodouble
.Jak widać w
zsh
kodzie (już w 2000 r., Kiedy dodano obsługę zmiennoprzecinkowązsh
):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: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:OK, ponieważ
awk
iyash
używajdouble
s tak jakzsh
, ale:nie OK, ponieważ
bc
używa dowolnej precyzji iksh93
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
:(
%g
, skrót%.6g
od domyślnego formatu wyjściowego dla elementów zmiennoprzecinkowychawk
). 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:
Chociaż porównania (nie) równości są na ogół niebezpieczne z zmiennoprzecinkowymi. Tutaj możemy się spodziewać
x
i1./3
być identycznymi, ponieważ są wynikiem dokładnie tej samej operacji.Również:
(jak yash nie zawsze zawierać
.
lube
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:
(
$((1e15))
rozwija się do1e+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.(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.
Co jest kolejną ilustracją tego, dlaczego tak naprawdę nie można używać operatora równości z zmiennoprzecinkowymi:
ksh93
Przypadek ksh93 jest bardziej złożony.
ksh93 używa
long double
s zamiast gdy jestdouble
dostępny.long double
s są gwarantowane przez C tylko co najmniej tak duże jakdouble
s. W praktyce, w zależności od kompilatora i architektury, najczęściej są to albo podwójna precyzja IEEE 754 (64 bity), jakdouble
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ą jakdouble
s, dostaniesz ten sam artefakt jak w przypadkuzsh
(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 zyash
wyjątkiem tego, że sytuacja jest lepsza, gdy interakcja z narzędziami działającymi zdouble
s, ponieważ ksh93 daje im większą precyzję niż potrzebują i zachowałaby tyle precyzji, co oni daj to.jest nadal „problemem”, ale nie:
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:Istnieją różnice w zachowaniu pomiędzy nimi
ksh93
,zsh
ayash
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łobyperl
...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:W takim przypadku używa formatu określonego w
OFMT
specjalnej zmiennej (%.6g
domyślnie), ale może być dowolnie duży: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).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
„szcalc
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.
źródło
double
wartości3fc0000000000000
i3fc0000000000001
. 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 rozszerzeniaBinarna reprezentacja obu
w podwójnej jest to samo.
Skompiluj ten kod (float.c):
Połącz z:
Uruchom, aby uzyskać:
Dokładnie taki sam plik binarny
9a9999999999b93f
dla 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:
I dostać:
Różnica polega na tym, w jakim momencie zaokrąglenie zostało wykonane. Zaokrąglanie w górę
d
zostało wykonane w ostatnim bicie w 0.10000000000000000L (64 bit 0xc.ccccccccccccccdp-7):Ale został wykonany na 52-tym:
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.
źródło
0.10000000000000001
ponad0.1
gdy używasz 17 cyfr znaczących jest bo0.10000000000000001
jest bliżej0.1000000000000000055511151231257827021181583404541015625
niż0.1
jest. To właśnieprintf("%.17g")
poprawnie daje.printf %.17g "$((0.1))"
.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.źródło