LangDev

프로그래밍 언어 개발에 관심 있는 사람들의 모임입니다.

부동소수점 계산 : Haskell과 다른 언어의 비교

2009-04-19 16:09:32

사건의 발단은 이러합니다. - 4주에 걸친 지옥의 중간고사를 대비하여 공부를 하던 중 계산을 해야할 일이 생겼지요. 그리 복잡하진 않지만 손을 풀기는 귀찮고 계산기도 없었습니다. 그리하여 옆에 있던 랩탑의 자판을 잡고 잠시 어떤 걸로 계산을 할까 고민을 했습니다. 요새 다시 Haskell을 보고 있는지라 GHCi 콘솔을 열고 수식을 입력하고 결과를 확인했지요.

Prelude> (5 * 0.08206 * 300 ) / 10
12.309000000000001

제일 끝의 1이 거슬리긴 했지만 어쨌건 값이 12.309라는 걸 파악할 수 있었습니다. 다음 식을 입력했죠.

Prelude> 12.309 * 10 / 61.545
1.9999999999999998

엥? 대충 봐도 2가 나와야 하는데 애매하게 나오네요. 게다가 오차 자체는 아까의 1/5 이지만 여튼 …2가 차이가 나네요. 그래서 역으로도 계산을 해봤습니다.

Prelude> 12.309 * 5
61.544999999999995

-_-; Haskell의 부동소수점은 도저히 못 쓰겠다 싶어서 Ruby의 irb 콘솔을 열었습니다.

irb(main):001:0> (5 * 0.08206 * 300) / 10
=> 12.309
irb(main):002:0> 12.309 * 10 / 61.545
=> 2.0
irb(main):003:0> 12.309 * 5
=> 61.545

깔끔하게 소수점 아래의 0은 떼어서 보여주네요. 이 맛에 Ruby를 쓰지요. 그런데 이 차이가 혹시 float와 double의 차이는 아닌가 싶어서 Eclipse CDT를 열었습니다. (MinGW 사용) C로는 세 가지로 테스트해 봤습니다.

printf("(5 * 0.08206 * 300) / 10 = %f\n", (5 * 0.08206 * 300) / 10);
printf("12.309 * 10 / 61.545 = %f\n", 12.309 * 10 / 61.545);
printf("12.309 * 5 = %f\n", 12.309 * 5);

---- output

(5 * 0.08206 * 300) / 10 = 12.309000
12.309 * 10 / 61.545 = 2.000000
12.309 * 5 = 61.545000

먼저 그냥 계산하고 형이 정해져 있는 변수에 담지 않고 바로 출력입니다. 잘 나오네요.

float f1 = (5 * 0.08206 * 300) / 10;
float f2 = 12.309 * 10 / 61.545;
float f3 = 12.309 * 5;

printf("(5 * 0.08206 * 300) / 10 = %f\n", f1);
printf("12.309 * 10 / 61.545 = %f\n", f2);
printf("12.309 * 5 = %f\n", f3);

---- output

(5 * 0.08206 * 300) / 10 = 12.309000
12.309 * 10 / 61.545 = 2.000000
12.309 * 5 = 61.544998

두 번째는 문제가 될 거라 예상되는 float에 저장한 다음 출력인데요, 앞의 둘은 문제가 없는데 세 번째 간단한 곱셈에서 문제가 생기네요. 게다가 오차는 Haskell의 경우보다 훨씬 크군요.

double d1 = (5 * 0.08206 * 300) / 10;
double d2 = 12.309 * 10 / 61.545;
double d3 = 12.309 * 5;

printf("(5 * 0.08206 * 300) / 10 = %f\n", d1);
printf("12.309 * 10 / 61.545 = %f\n", d2);
printf("12.309 * 5 = %f\n", d3);

---- output

(5 * 0.08206 * 300) / 10 = 12.309000
12.309 * 10 / 61.545 = 2.000000
12.309 * 5 = 61.545000

마지막으로 문제가 없으리라 기대한 double입니다. 내부 비트야 모르겠지만 역시 잘 출력되네요. 그래서 의심이 생겼습니다. Haskell처럼 유효숫자를 늘려서 출력하면 어떨까요?

double d1 = (5 * 0.08206 * 300) / 10;
double d2 = 12.309 * 10 / 61.545;
double d3 = 12.309 * 5;

printf("(5 * 0.08206 * 300) / 10 = %.20f\n", d1);
printf("12.309 * 10 / 61.545 = %.20f\n", d2);
printf("12.309 * 5 = %.20f\n", d3);

---- output

(5 * 0.08206 * 300) / 10 = 12.30900000000000100000
12.309 * 10 / 61.545 = 1.99999999999999980000
12.309 * 5 = 61.54499999999999500000

뒤의 0을 떼고 보니 Haskell과 동일한 값이네요. 처음에 제가 겪었는 오차는 예상과는 다르게 float가 아니라 double로 게산했을 때 오차였습니다. (참고로 윈도 계산기로도 잘 게산이 됐습니다. 엑셀은 … 귀찮아서 생략)

여기서 드는 의문점이 있네요. 일단 부동소수점 계산 자체가 완벽할 수는 없지요. 그렇다면 Haskell처럼 기계(machine)의 오차를 그대로 보여주는 게 좋을까요, 아니면 Ruby처럼 적당히 오차를 수정하여 보여주는 게 좋을까요? 혹은 C처럼 프로그래머가 오차 범위를 지정하게 하는 게 좋을까요?

뜨거운 가슴, 차가운 머리

트랙백 주소: http://langdev.net/post/trackback/279

  1. 엽우의 생각 from leafriend's me2DAY
  1. 김우승 2009-04-19 17:15:48

    정책의 차이일 뿐입니다. Haskell과 Ruby는 연산결과값을 보여주기에 printf를 사용한 C와는 좀 거리가 있네요. Haskell처럼 보여줄 것이냐 Ruby처럼 보여줄 것이냐는 양자간의 입장 차이일 뿐이지요. 아마 조정하는 방법이 다 있을 겁니다. 그리고 C에서 Ruby처럼 보여주고 싶다면 %g를 쓰시죠.

  2. jong10 2009-05-06 19:03:59

    %g는 가장 글자 길이가 짧은 표현을 주기 땜에, 100000000를 찍으면 1e+008가 나와서 슬퍼요. ㅠㅠ 소수점을 간결히 해주면서, 윗부분을 길게 찍어주는 형식 지정자가 printf류에도 있으면 좋을텐데요. 때로는, 저만 그런 욕구(?)를 느끼는지도 궁금.

  3. 김우승 2009-05-07 00:34:54

    저도 느꼈었는데 쓰다보니까 더이상 필요성을 못느낍니다. 수가 커지면 그에 적절한 표현으로 대체하는게 보기 좋다고 생각하기 때문이죠.

  4. wookay 2009-05-07 12:55:15

    오랜만에 실행해 봤습니다

    Prelude> (5 * 0.08206 * 300 ) / 10 :: Float
    12.309
    
    
  5. jong10 2009-05-10 01:15:35

    이와는 조금 다른 관점인데, 요즘 언어들은 BigInteger로의 변환이 자유로운 것 처럼, 언어 레벨에서 숫자 자체를 분수로 다뤄야 하는 것이 아닌가 하는 생각도 들어요. 메모리는 넉넉하잖아요. 가감승제에서의 속도는 좀 그렇겠지만..

  6. 김우승 2009-05-11 01:33:43

    쿨럭~ 최근에 입사면접 떨어진데가 유리수-무리수 문제를 coding하라고 했었는데…

    필요에 따라서 쓰면 되는데. 정수-큰정수 변환은 어렵지가 않습니다만 제 생각에 부동소수점-유리수는 변환이 조금 곤란한 점이 있는 것 같아요. 부동소수점은 연산손실이 있어서 이부분을 어떻게 처리할지가 문제잖아요.

    일반 정수에 비해서 부동소수점의 연산속도가 대략 1/4 정도였던 느낌을 받은 적이 있는데 요새는 모르겠군요.

    부동소수점은 하드웨어의 강력한 지원을 받아서 그정도 성능을 냅니다. 이걸 유리수-무리수로 만든다면 성능이 좀 아행행일듯…

    그리고 부동소수점을 적극적으로 쓰면 장난아닌게 부동소수점이예요. 행렬 생각하면 죽음. 시간도 공간도 말이죠.

  7. 김우승 2009-05-14 00:17:04

    생각이 좀 바뀌어서 추가로 글씁니다. 질문하신 것은 부동소수점과 실제 실수의 변환을 이야기하신 것은 아니네요.

    확실히 큰 의미가 있는 작업이라고 생각합니다. 부동소수점은 확실히 공학적 작업을 위해 적합하지 사람들이 인지하는데 적절한 것은 아닙니다.

    알고리즘을 할 때에도 항상 부동소수점의 기계무한소(machine epsilon)는 쉬운 대안이 없다라고 이야기하는데 사실 그게 부동소수점이기 때문이죠.

    언어 레벨에서 다루는 것이 좋으냐 아니면 라이브러리 차원에서 다루는 것이 좋으냐는 언어의 스타일에 따라 다를 것 같습니다. C/C++는 하드웨어와 좀더 가까우므로 언어차원에서 다루는 것은 어려울 것 같고요. 홍민희씨라면 그것이 중요한 분야에서 어떤 언어든 언어를 확장해서 넣는 작업을 할 것 같네요… ^_^

  8. reeseo 2009-05-14 05:24:36

    첫 식에 등장하는 0.08206 자체가 이미 일말이라도 보정된 역사가 있는 근사치였다면 아무 의미 없는 작업이겠습니다만 (아마 그래서 알고 계시면서 안쓰신 방법인 듯 합니다만), 정말로 정확하게 0.08206을 의미했다면 다음과 같이 해볼 수도 있겠죠:

    Prelude> :m + Ratio
    Prelude Ratio> 5 * (0.08206::Rational) * 300 / 10
    12309%1000
    Prelude Ratio> (12309%1000) * 10 / (61.545::Rational)
    2%1
    
    

    Exact arithmetic을 고효율 근사치 연산/표현과 철저하게 구분하거나 아예 언어의 core 수준에서 기본으로 삼는 개념에는 저도 관심이 많습니다만, 범용 PL에서 고려하기에는 대다수 프로그래머들에게 실질적인 인기가 별로 없나봅니다. ㅜㅜ Python에 제안되었다가 부결된 예(PEP239,240)도 있고, 현재 활발히 쓰이는 곳은 Mathematica나 Maxima 등 (symbolic computation에 기반을 둔 일부) CAS류 정도인 것으로 알고있습니다. 대신, 위 Haskell의 예처럼 모듈 형식으로는 이미 많이들 지원하고있는 듯 합니다.

    여담입니다만, exact arithmetic을 실제로 구현하다보면 겪게되는 문제 중 아마도 핵심은, 어떤 체계(정수, 유리수, 유리수와 surd number)로 한정지어 구현하든 간에 거기에 산술 체계를 만드는 순간 그 체계의 범위를 벗어나는 수가 반드시 튀어나온다는, 어디서 많이 들어봤던 정리와 아주 닮은 일이 벌어진다는 점이 아닐까 합니다. 결국 어느 선에서는 타협(닫혀있지 않은 연산을 partial function 등으로 정의하는 비겁한 변명)을 해야만 하죠. -_-;;

    PS: 작년에 여기 가입하고 인사글도 남겼던 것으로 기억하는데, 로그인이 안되길래 확인해보니 제 인사글도 안보이네요. @.@ 다시(?) 가입했습니다. ^^;;

  9. lifthrasiir 2009-05-14 20:49:51

    안녕하세요~

    파이썬은 2.6부터 유리수 자료형을 지원합니다. fractions 모듈을 찾아 보세요. 아무래도 저도 헷갈리는 걸로 봐서 파이썬 매뉴얼을 다시 정독할 때가 된 듯…

  10. reeseo 2009-05-14 21:51:24

    안녕하셔요~ ^^

    2.6에 들어간 fractions 모듈을 예전에 (아마도 2.6이 테스트 버전이었을 때?) 한 번 봤었고, 당시에 이 모듈이 decimal 모듈에 기반을 둔 ‘가짜 유리수’ 혹은 ‘분수형 근사치’로서 결국 exact arithmetic과는 무관함을 확인하고 실망했던 기억이 있었습니다….

    …만, 방금 혹시나 해서 다시 확인해보니 제가 기억하고있는 내용은 from_float나 from_decimal등의 함수로 따로 정의되어있고, 기본적으로는 exact arithmetic이 맞군요. @.@ 당시에 뭘 어떻게 착각했던건지 모르겠습니다만, 아무튼 덕분에 행복한 사실 하나를 알게되었습니다 ^^

    PS: 어쩌면, 비슷한 시기에 Python에 채택됐던 (임의 정밀도 근사치 연산으로서의) decimal 모듈이 당시에 혼자 만들어 쓰던 유리수 모듈의 ‘exact decimal’의 개념과 완전히 어긋났던 것에 삐졌던 기억을, 시간이 지나면서 애꿎은 fractions 모듈에 대한 불만으로 잘못 기억하게된건지도 모르겠습니다. (치매인가봅니다 ㅜㅜ) Exact decimal은 별게 아니고, 그냥 모든 유리수는 (진법에 상관 없이) 반드시 유한소수나 순환소수에 대응하고 역으로도 반드시 대응한다는 점을 구현한, 유리수에 대한 또다른 표현법/입력법이었습니다.

목록보기

← 2009년 5월 19~29일 이것저것 | 2009년 5월 13일 이것저것 →