프로그래밍 실수의 크기 비교하기
부동소수
프로그래밍 언어들에 따라 real, double, float, extended 등으로 이름지어지는 실수實數는 자릿수의 크기에 따라 정밀한 정도엄밀하게는 범위가 달라진다. 그래서 부동소수라고 한다. 여기에서 ‘부동’은 같지 않다는 不同이나 움직이지 않는다는 不動이 아니라 떠다닌다는 浮動floating-point이다. 예를 들어 어느 프로그래밍 언어의 실수형이 다섯 자리의 정확도precision를 보장한다고 하면 12,345.67890의 경우 소수들의 정확도는 담보되지 않는다. 그러나 0.67890의 경우에는 담보된다. 이러한 정확도의 경우 1 + 0.67890 = 1.67890은 false를 반환할 가능성이 크다. 이렇게 부동소수의 정밀한 정도는 단순하게 ‘소수 몇 번째 자리까지’가 아니라 전체 길이에 따라 결정된다.
컴퓨터가 실수를 다루는 이러한 특성을 제대로 이해하지 못하면 대형 참사가 일어날 수도 있다. 2011년 삼성sds는 정부의 차세대교육행정정보시스템neis를 개발해 납품했는데 이 시스템이 오작동했다. 원인은 부동소수의 소수를 제대로 처리하지 못했던 거였다. 컴퓨터가 처리하는 부동소수에는 위에 설명된 거와 같은 정밀도의 문제가 있기 때문에 1이 0.9999999999으로 처리되거나 1.0000000001으로 처리되는 경우가 흔하다. 위 시스템은 이러한 값들을 가지고 어느 게 더 큰가, 작은가를 단순하게 부등호로만 판단하여 학생들의 성적 순서를 엉뚱하게 연산했다. 대학교에서 컴퓨터를 전공한 사람이라면 위와 같은 실수를 할 일이 거의 없다. 커다란 소프트웨어 회사들은 건설회사들이 흔히들 그렇게 하듯 자신들이 계약한 개발 건을 조그만 회사에 도급하기도 한다. 조그만 소프트웨어 개발 회사에는 대학교에서 전공하지 않고 학원에서 몇 달 배우고 들어와 일하는 사람들이 많다. 이들 가운데에는 저렇게 기본도 되어 있지 않은 수준의 프로그래머들도 있게 마련이다. 특히 우리 공동체에는 비정상적으로 자바 수요가 커서 학원을 통해 양산된 자바 프로그래머들이 많은데 사정이 이렇다 보니 자바 쪽에서 사고들이 많이 터진다.
엡실론
실수를 비교할 때 생기는 문제를 막기 위해 흔히 epsilon‘입실론’은 틀린 발음이라는 이름의 미세수를 만들어서 상수나 변수로 쓴다. 미지수를 x라 하듯이 관례적으로 그리스 문자 e를 미세수로 이용한다. 예를 들어 어떤 연산의 결과가 1.5 이상인 때 조건이 만족되는 경우 그 결과가 3/2라면 논리적으로는 참이어야 하지만 사실적으로는 cpu가 1.5000000001이나 1.4999999999로 처리하여 조건 만족 여부가 우연에 따라 결정될 수 있다. 이때 엡실론을 이용하면 아래와 같이 표현할 수 있다.
value = 3 / 2;
epsilon = 0.05;
if value > 1.5 - epsilon then ... // true;
델파이
델파이에는 실수 변수형으로 real, double, extended가 있는데 이들의 정밀한 범위는 순서대로 넓고 메모리도 더 차지한다. real과 double은 서로 같다. 같은데 이렇게 다른 이름들로 있는 건 다른 언어를 이용하는 사람들에 대한 배려일 수 있다. double이란 c계열에서 이용되는 이름이다. 아니면 델파이가 발전되어 오는 과정에서 오래전 사용되었던 특정 변수 타입을 하루 아침에 없애 버리는 경우 코드의 하위 호환성이 손상되므로 이를 막기 위한 것일 수도 있다. 그래서 레퍼런스에는 ‘현재 시점in the current implementation‘으로 같다고 표현되어 있다.
extended는 델파이에서 주로 쓰이는 실수 변수형이다. 윈도우즈 32-비트 플랫폼에서 10 바이트로 제일 큰 변수형이다. 10~20자리까지 유효하여 제일 정밀하다. 근데 이게 64-비트 플랫폼에서는 8 바이트로 줄어서 double과 같다. 64-비트 플랫폼에는 10 바이트짜리 실수 변수형이 아예 없다.
실수의 크기를 비교하기 위해 System.Math.CompareValue 메떠드를 쓸 수도 있다. 이 메떠드의 경우 엡실론을 설정하여 이용할 수 있지만 입력하지 않고 메떠드가 자동으로 계산하게 할 수도 있다. 그러나 자동으로 계산하게 되면 그 값이 너무 미세해져서 유효 범위를 벗어나 엉뚱한 결과를 부를 수 있으므로 이용하지 않는 게 좋다. 예를 들어 0.45는 0.450000000000046으로 저장될 수 있는데 CompareValue가 계산한 엡실론은 0.000000000000046보다 작다. 이 경우 아래의 문제가 생긴다.
value = 0.45;
if CompareValue(value, 0.45) = EqualsValue then ... // false
두 값들이 같다고 연산되어야 하지만 이들 값의 오차가 엡실론의 값보다 크기 때문에 연산은 같지 않다고 작동한다. 결국 엡실론 이용은 유효 범위 결정의 문제로 귀결되는데 이는 다시 유효 범위를 어떻게 결정할 것인가 하는 문제를 초래한다. 간단하게 생각하면 된다. 사람이 최종적으로 확인하는 값의 소수 자리를 유효 범위로 보고 그 다음 자리에 5를 설정한 값을 쓰면 된다. 위의 첫 번째 예에서 기준이 되는 값은 1.5이므로 유효 범위는 소수 첫째 자리라서 엡실론을 0.05로 한 거다. 0.01이 아닌 것에 유의한다.
c#
.네트에는 실수 변수형으로 float, double, decimal이 있으며 정밀한 범위는 역시 순서대로 넓고 메모리도 더 차지한다. decimal은 엡실론을 쓰지 않아도 정수처럼 그대로 값을 믿고 써도 된다. 그러나 성능이 떨어진다. float와 double 가운데 어떤 게 더 빠르냐에 대해서는 다양한 논의들이 가능하다. 단순하게 보면 작은 변수인 float를 읽고 쓰는 게 더 빠르지만 복잡한 하드웨어와 오퍼레이팅 시스템의 구조 덕에 늘 그런 건 아니다. 일부 교재에는 float가 더 빠르다고 나와 있지만 일반적으로 그렇다고 하는 건 틀린 이해다. 마이크로소프트의 레퍼런스에 decimal의 성능에 대해서는 명시적으로 나와 있지만 float가 double보다 빠르다는 내용은 없다.
.네트는 아예 Double.Epsilon이라는 ‘상수’를 제공한다. 여기에서 Double은 실수 변수형 double이 아니라 구조체 Double이고 Epsilon은 이 구조체의 필드다. 그러나 이거는 실수로 표현할 수 있는 가장 작은 값이므로 델파이를 통해 자동으로 계산되는 엡실론보다 더 작아서 크기를 비교하는 데에는 사실상 쓸모가 없다. 레퍼런스는 이런 경우에 Double.Epsilon을 이용하지 말라고 권하고 있다.