zsh에서 $((0.1))이 0.10000000000000001로 확장되는 이유는 무엇입니까?

zsh에서 $((0.1))이 0.10000000000000001로 확장되는 이유는 무엇입니까?

존재하다 zsh:

$ echo $((0.1))
0.10000000000000001

그리고 부동 소수점 연산 확장이 있는 다른 쉘에서는:

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

또는 awk:

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

왜?


이건 후속작이야채팅 토론

답변1


긴 이야기 짧게

zshdouble정보가 완전히 보존되고 산술 표현식에 안전하게 다시 입력될 수 있도록 부동 소수점 산술을 평가하는 데 사용되는 이진수의 10진수 표현을 선택합니다. 이것은 화장품을 희생하여 달성됩니다. 이를 위해서는 17개의 유효 숫자가 필요하며 재입력 시 부동 소수점 숫자로 처리되도록 확장에 항상 .등이 포함되도록 해야 합니다.e

double이 "완전 정밀도" 십진수 표현은 이진 정밀도 기계 숫자와 사람이 읽을 수 있는 숫자 사이의 중간 형식 으로 생각할 수 있습니다 . 부동 소수점 소수 표현을 이해하는 모든 도구가 이해하는 중간 형식입니다.

0.1이 산술 표현식에 사용될 때 double의 0.1에 가장 가까운 17자리 십진수 표현은 0.100000000000000001입니다. 이는 double의 정밀도 제한과 반올림으로 인해 발생하는 아티팩트입니다.

다른 쉘은 모양 면에서 특권을 가지며 십진수로 변환할 때 일부 정보를 잃습니다(추가 제약 조건 내에서 가능한 한 많은 정밀도를 유지하려고 노력하지만). 두 방법 모두 장점과 단점이 있습니다. 자세한 내용은 아래를 참조하세요.

awk쉘이 아니며 부동 소수점을 조작할 때 이진수와 십진수 표현 사이를 끊임없이 앞뒤로 변환할 필요가 없기 때문에 그러한 문제가 없습니다.

zsh 방법

zsh산술 연산은 다른 많은 프로그래밍 언어( , 포함) 및 부동 소수점 숫자를 처리하는 셸에서 사용되는 많은 도구(예: , ... )에서와 같이 yash이러한 ksh93숫자 awk의 이진 표현에 대해 수행됩니다 .printf

이러한 작업은 C 컴파일러에서 지원되고 대부분의 아키텍처에서 프로세서 자체에서 수행되므로 편리하고 효율적입니다.

zshdouble실수의 내부 표현에는 C 유형을 사용합니다 .

대부분의 아키텍처(및 대부분의 컴파일러)에서는 IEEE 754 배정밀도 이진 부동 소수점을 사용하여 구현됩니다.

이들의 구현은 1.12e4 엔지니어링 표기법 십진수와 다소 유사하지만 십진수(진수 10)가 아닌 이진수(진수 2)로 구현됩니다. 가수는 53비트(암시적 1비트)이고 지수는 11비트(부호 비트 포함)입니다. 이는 일반적으로 필요한 것보다 더 많은 정확도를 제공합니다.

이와 같은 산술 표현식이 평가되면 1. / 10(여기에는 피연산자 중 하나로 리터럴 부동 소수점 상수가 있음) zsh내부적으로 리터럴 십진수 표현에서 doubles(표준 strtod()함수 사용)로 변환되고 실행되어 새로운 double.

1/10은 0.1 또는 1e-1로 십진수로 표현될 수 있지만, 십진수로 1/3을 표현할 수 없는 것처럼(3, 6, 9진수는 가능합니다), 1/10도 이진수로 표현할 수 없습니다(10은 그렇지 않기 때문입니다). 2 전력). 1/3이 10진수로 0.333333 adlib인 것처럼 , 1/10은 2진수로 .0001100110011001100110011001 adlib 또는 1.10011001100110011001 adlib p-4입니다( 4p-4 는 10진수).

이 값 중 52비트만 저장할 수 있으므로 1001...a의 1/10은 double1.1001100110011001100110011001100110011001100110011010p-4가 됩니다(마지막 2자리는 반올림됩니다).

이것은 s로 얻을 수 있는 가장 가까운 1/10 표현입니다 double. 이를 다시 십진수로 변환하면 다음과 같은 결과를 얻습니다.

#         1         2
#12345678901234567890
.1000000000000000055511151231257827021181583404541015625

이전 double것(1.1001100110011001100110011001100110011001100110011001p-4는 다음과 같습니다.

.09999999999999999167332731531132594682276248931884765625

그리고 그 이후의 것(1.1001100110011001100110011001100110011001100110011011p-4):

.10000000000000001942890293094023945741355419158935546875

그렇게 가깝지는 않습니다.

이제 zsh먼저 명령줄 해석기인 쉘이 있습니다. 조만간 산술 표현식의 결과를 부동 소수점 숫자로 명령에 전달해야 합니다. 셸이 아닌 프로그래밍 언어에서는 double호출하려는 함수를 전달합니다. 하지만 쉘에서는 다음과 같이만 전달할 수 있습니다.명령에. 원시 바이트 값 double은 NUL 바이트를 포함할 가능성이 높으며 명령은 이를 어떻게 처리할지 모르기 때문에 전달할 수 없습니다 .

따라서 이를 명령이 이해할 수 있는 문자열 표현으로 다시 변환해야 합니다. IEEE 754 이진 부동 소수점 숫자를 쉽게 나타낼 수 있는 C99 0xc.ccccccccccccccdp-7 부동 소수점 16진수 표기법과 같은 기호가 있지만 아직 널리 지원되지 않으며 일반적으로 대부분의 사람들에게 의미가 없습니다. 사람들은 0.1) 위의 광경을 인식할 것이다. 따라서 산술 확장의 결과는 $((...))실제로 10진수 표기법으로 표현되는 부동 소수점 숫자입니다.

이제 .1000000000000000055511151231257827021181583404541015625는 약간 길며 doubles(및 산술 표현식의 결과)가 그다지 높은 정밀도를 갖지 않는다는 점을 고려하면 그렇게 높은 정밀도를 제공하는 것은 의미가 없습니다. 실제로 .1000000000000000055511151231257827021181583404541015625, .100000000000000005551115123125782 또는 이 경우 0.1도 동일한 double.

yash이와 같이 (부동 소수점 연산을 위해 내부적으로 s를 사용하는 ) 15자리로 자르고 반올림하면 double0.1을 얻지만 다른 두 doubles에 대해서도 0.1을 얻습니다. 따라서 우리는 3이기 때문에 이들을 구별할 수 없습니다. 숫자가 다르기 때문에 정보가 손실됩니다. 16비트로 잘라도 여전히 2개의 다른 doubles를 얻게 되어 0.1이 됩니다.

IEEE 754 double에 저장된 정보의 손실을 방지하려면 유효 십진수 17자리를 보존해야 합니다. ~처럼배정밀도 위키피디아 기사(IEEE 754의 주요 설계자인 William Kahan의 논문 인용):

IEEE 754 double을 유효 숫자가 17개 이상인 10진수 문자열로 변환한 다음 다시 double 표현으로 변환하는 경우 최종 결과는 원래 숫자와 일치해야 합니다.

반대로, 더 적은 비트를 사용하면 위의 예와 같이 double다시 변환하면 동일한 값을 얻지 못하는 일부 이진 값이 있습니다 .double

이것이 zsh바로 이진 형식의 전체 정밀도를 double산술 확장의 결과에 의해 제공되는 십진수 표현으로 유지하도록 선택하여 다시 사용할 때 무언가(예: 또는 zsh의 자체 산술 표현식...)로 변환할 수 있도록 하는 awkprintf "%17f"입니다. ) a로 돌아가서 double돌아와도 똑같습니다 double.

코드에서 볼 수 있듯이 zsh(부동 소수점 지원이 추가된 2000년 이후였습니다 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.
     */

.또한 산술 표현식에서 다시 사용될 때 부동 소수점 숫자로 처리되도록 자르고 추가할 때 소수 부분이 없는 부동 소수점 숫자를 확장한다는 점도 알 수 있습니다 .

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

그렇지 않은 경우 산술 표현식에서 재사용되면 부동 소수점 숫자가 아닌 정수로 처리되어 사용되는 연산의 동작에 영향을 미칩니다(예: 2/4는 정수 나누기이므로 0과 2를 생성합니다./4 부동 소수점 숫자 나누기이며 결과는 0.5입니다.

이제 유효 숫자를 선택한다는 것은 0.1을 입력으로 사용하면 이진수 1.10011001100110011001100110011001100110011010p-4 double(0.1에 가장 가까운 값)가 0.100000000000001이 되어 인간에게는 좋지 않게 보인다는 의미입니다. 오류가 다른 방향인 경우 상황은 더욱 악화됩니다. 예를 들어 0.3은 0.29999999999999999가 됩니다.

지원되는 애플리케이션에 해당 번호를 전달할 때 반대의 문제도 있습니다.s 보다 정밀도가 높으면 double실제로 0.000000000000001 오류(0.1과 같은 사용자 입력 값에서)를 전달한 다음 그 오류가 중요해집니다.

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

좋습니다. s 를 사용하는 awk것과 똑같기 때문입니다 . 하지만:yashdoublezsh

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

bc내 시스템에서는 임의 정밀도와 확장 정밀도를 사용하기 때문에 좋지 않습니다 .ksh93

이제 원래 십진수 입력이 0.1(1/10)이 아니라 0.11111111111111111(또는 1/9의 다른 임의의 근사치)인 경우 테이블이 뒤집어져 부동 소수점 숫자의 동등 비교가 매우 절망적임을 나타냅니다.

휴먼 디스플레이 아티팩트 문제는 정밀도를 지정하여 해결할 수 있습니다.표시할 때(모든 계산이 완전한 정밀도로 완료된 후) 예를 들어 다음을 사용합니다 printf.

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

( , 의 부동 소수점 숫자에 대한 기본 출력 형식의 약어와 유사 %g). 이는 또한 정수 부동 소수점 숫자의 추가 후행을 제거합니다.%.6gawk.

yash(및 ksh93의) 메소드

yash정확성을 희생하면서 아티팩트를 제거하기로 선택한 십진수 15자리는 우리의 $((0.1))경우와 같이 숫자를 십진수에서 이진수로 변환하거나 다시 반대로 변환할 때 이 아티팩트가 나타나지 않도록 보장하는 유효한 십진수 중 가장 높은 수입니다.

실제로 이진수의 정보는 십진수로 변환되면 손실되며, 이로 인해 다른 형태의 아티팩트가 발생할 수 있습니다.

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

(내부) 동등 비교는 일반적으로 부동 소수점에 대해 안전하지 않습니다. 여기서는 완전히 동일한 작업의 결과이므로 동일하다고 x기대할 수 있습니다.1./3

반품:

$ 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

(yash는 부동 소수점 결과의 십진수 표현에 항상 .OR를 포함하지 않으므로 e다음 산술 연산은 결국 정수 연산이나 부동 소수점 연산이 될 수 있습니다.)

또는:

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

( float로 $((1e15))확장하고 100000000000000으로 확장하면 정수처럼 작동하며 실제로 부동 소수점 곱셈이 아닌 정수 곱셈을 수행하므로 오버플로가 발생합니다.)1e+15$((1e14))

위에 표시된 것처럼 표시 정밀도를 줄여 아티팩트 문제를 해결하는 방법이 있지만 zsh, 다른 셸에서는 정밀도 손실을 복구할 수 있는 방법이 없습니다.

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

(여전히 15자리만 남았습니다)

그럼에도 불구하고, 잘림이 아무리 짧아도 산술 확장의 결과로 항상 아티팩트가 발생할 수 있습니다. 오류는 부동 소수점 표현에 내재되어 있기 때문입니다.

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

부동 소수점과 함께 항등 연산자를 실제로 사용할 수 없는 이유에 대한 또 다른 예는 다음과 같습니다.

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

크쉬 93

ksh93의 상황은 더 복잡합니다.

ksh93은 가능한 경우 long doubles를 대신 사용합니다. s는 C에 의해서만 적어도 s만큼 커지는 것이 보장됩니다. 실제로는 컴파일러와 아키텍처에 따라 일반적으로 s와 같은 IEEE 754 배정밀도(64비트), IEEE 754 4중 정밀도(128비트) 또는 확장 정밀도(80비트 정밀도, 일반적으로 128비트에 저장됨) 입니다. ) ksh93이 x86에서 실행되는 GNU/Linux 시스템용으로 구축된 것과 같습니다.doublelong doubledoubledouble

십진수로 완전하고 명확하게 표현하려면 각각 17, 36 또는 21개의 유효 숫자가 필요합니다.

ksh93은 유효 숫자 18자리에서 잘립니다.

long double현재는 x86 아키텍처에서만 테스트할 수 있지만 s 와 유사한 시스템에서는 double와 동일한 결과를 얻을 수 있다고 이해합니다 zsh(더 나쁘게는 17 대신 18자리를 사용합니다).

doubles의 정밀도가 80비트 또는 128비트 일 때 s와 동일한 문제가 있습니다 . ksh93은 필요한 것보다 더 많은 정밀도를 제공하고 그만큼의 정밀도를 유지하기 때문에 yashs를 사용하는 도구와 상호 작용할 때 더 좋습니다 .double

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

여전히 "문제"이지만 다음은 아닙니다.

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

괜찮아요.

그러나 동작이 차선책인 곳은 typeset -F<n>/-E<n>사용될 때입니다. 이 경우 ksh93은 <n>15보다 큰 값을 요청하더라도 변수에 값을 할당할 때 유효 숫자 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

동작에 차이가 있으며 이는 로케일의 10진 기수 문자(3.14 또는 3,14가 ksh93사용 /인식되는지 여부)를 처리할 때 산술 표현식 내에서 산술 확장 결과를 다시 입력하는 기능에 영향을 미칩니다. zsh는 사용자의 로케일에 관계없이 산술 표현식에서 확장된 결과를 항상 사용할 수 있다는 점에서 다시 일관성을 갖습니다.zshyash

awk그 중 하나야프로그래밍 언어이것은 쉘이 아니며 부동 소수점 숫자를 처리합니다. 동일하게 적용됩니다 perl...

해당 변수는 문자열로 제한되지 않으며 이제 일반적으로 숫자를 내부적으로 이진수로 저장합니다 double( gawk임의 정밀도 숫자도 확장으로 지원됩니다). 문자열 십진수 표현으로의 변환은 다음과 같은 경우에만 발생합니다.인쇄다음과 같은 숫자:

$ awk 'BEGIN {print 0.1}'
0.1

이 경우 특수 변수 OFMT( %.6g기본적으로)에 지정된 형식을 사용하지만 임의로 클 수 있습니다.

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

subtr()또는 문자열 연산자(예: 연결, ...)를 사용할 때와 같이 숫자를 문자열로 암시적으로 변환할 때 index()CONVFMT 변수(정수 제외)가 사용됩니다 .

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

또는 printf명시적으로 사용될 때.

10진수와 2진수 표현 사이를 앞뒤로 변환하지 않기 때문에 일반적으로 내부 정밀도 손실 문제는 없습니다. 출력 측면에서는 어느 정도의 정밀도를 부여할지 결정할 수 있습니다.

결론적으로

마지막으로 개인적인 의견만 말씀드리겠습니다.

쉘 부동 소수점 연산은 제가 자주 사용하는 것이 아닙니다. 대부분의 경우 부동 소수점 숫자를 6자리 정밀도로 인쇄하는 자동 로딩 계산기 기능을 zsh통해 이루어집니다. zcalc대부분의 경우 소수점 뒤 처음 3자리는 이러한 사용 유형에 대한 노이즈일 뿐입니다.

산술 확장에는 높은 정밀도가 필요합니다. 일부 아티팩트를 피하면서 완전한 정확도인지 아니면 최고 정확도인지는 그다지 중요하지 않을 수 있습니다. 특히 아무도 쉘을 사용하여 많은 부동 소수점 계산을 수행하지 않을 것이라는 점을 고려하면 더욱 그렇습니다.

소수점으로의 라운드트립으로 인해 추가 오류가 발생하지 않는다는 사실이 나에게 위안이 되지만 zsh, 확장의 결과가 산술 표현식에 안전하게 사용될 수 있다는 것을 아는 것이 더 중요하다는 것을 알았습니다. 부동 소수점 숫자는 부동 소수점으로 유지됩니다 ,. 스크립트는 십진수와 동일한 로케일에서 사용될 때 계속 작동합니다.


1 zsh는 10 이외의 진수에서 산술 확장을 수행할 수 있는 유일한 Korn 유사 쉘이지만 이는 정수에서만 작동합니다.

답변2

짧은 대답은 다음과 같습니다. 1/10은 2진법의 단순한 분수가 아니며, 유한한 2진법 숫자로 표현될 수 없습니다.

zsh분명히 내부 부동 소수점 데이터 표현을 사용하여 부동 소수점 표현식과 형식 변환을 평가합니다.

관련 정보