C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
분야별 포럼
C++빌더
델파이
파이어몽키
C/C++
프리파스칼
파이어버드
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

자유게시판
세상 살아가는 이야기들을 나누는 사랑방입니다.
[19292] 부동소수점 계산 관련 논란에 대해
박지훈.임프 [cbuilder] 11581 읽음    2011-03-28 15:21
솔직히 이 문제에 대해 아무런 글을 쓰고 싶지 않았습니다. 요즘 전산학 개론 관련 과목들에서는 어떻게 배우는지 몰라도, 프로그래밍 언어의 기본을 배웠다면 논란거리가 되지 않는 내용이니까, 당연히 원칙적인 문제로 잘 귀결되리라 생갃했습니다. 요즘 제가 몇군데 프로젝트를 동시에 뛰다보니 글을 길게 써서 설명해야 하는 건에 대해 대응할 시간도 없었고요. 근데 이게 델파이나 C++빌더를 공격하는 이유로까지 거론되니 어이가 없어서, 한번은 짚고 넘어가야겠네요.

김태선님 등 몇분이 잘 설명해주셨습니다만, 결론부터 말해서, 부동소수점 숫자의 계산의 오차 문제는 프로그래밍 언어의 버그를 논할 문제가 아닙니다. 우리가 일반적으로 사용하는 모든 컴퓨터들은 2진수 체계를 가지고 있기 때문에, 소숫점 이하로 내려가면 동일하게 표현할 수 없는 10진수들이 많습니다. 그렇다고 10진수를 모사하기 위해 억지로 보정을 하면 다른 엉뚱한 부작용들이 생깁니다. 그래서 보정을 하느냐 마느냐는 아주 민감한 선택입니다. VC++은 그 민감한 선택을 다른 대부분의 C/C++ 컴파일러들과 다르게 선택한 거구요.

박우성님이 제기하신 문제의 핵심은, 구질구질한 부분들을 빼고 핵심적인 부분만 잘라내면 다음의 코드입니다.
2./ 1000.0
이 숫자는, 2진수 기반 컴퓨터에서는 "존재하지 않습니다". 표현할 수 없기 때문에 존재하지 않는 값입니다. 초등학교 때부터 고등학교 때까지 배운 10진수 수학으로는 아주 단순한 문제로, 누구나 간단히 0.002라고 대답을 할 겁니다. (만약 정수끼리만 연산을 해서 2/1000을 했다면 예상한 결과가 나옵니다)

물론 이걸 실제로 코드로 짜서 printf로 출력해보면 10진수의 상식대로 나타나는 것처럼 보입니다.
printf("%f", 2/ 1000.0);
그런데, 아래와 같이 printf의 출력 포맷을 강제로 더 자세하게 지정해서 출력해보면 다른 결과가 나옵니다.
printf("%22.21f\n", 2/ 1000.0);
0.0020000000000000000000000042 뭐 이렇게 나옵니다. 이건 정확히 말해서 십진수에서 2./ 1000.0의 결과가 이렇다는 것이 아니라, 2진수 부동소수점 연산에서는 십진수 2./1000.의 정확한 값을 표현하는 것이 원칙적으로 불가능하기 때문에 오차가 들어간 것입니다.

왜 이렇게 나올까요? 이건 precision의 문제도 아닙니다. 2진수 체계에서는 아무리 precision을 높여도 똑같은 결과만 나옵니다. 거꾸로, 2진수에서는 아주 단순하게 소숫점 이하 몇자리로 나오는 소수도 10진수로는 무한 소수로 나오는 경우도 아주 흔합니다. 가끔 있는 게 아니라 아주 흔합니다. 이건... C/C++의 문제가 아니라 전산학 개론의 상식입니다. 소숫점 위의 값들은 10진수건 2진수건 서로 같은 값을 표현할 수 있지만, 소숫점 아래는 상황이 완전히 다릅니다.

부동소수점 값을 연산할 때는 이렇게 진법간의 차이로 인한 논리적인 값 차이 문제가 발생할 수 있다는 것을 항상 염두에 둬야 합니다. 요즘에는 모르겠습니다만 예전에는 부동소수점을 거론하는 거의 모든 초급서에 이 내용이 적혀 있었습니다.

이걸 "사람이 이해하는 것과 같아야지 이유야 어쨌든 사람이 생각하는 것과 다르게 나오니까 버그다"라고 생각하신다면, 아예 부동소수점을 쓰지 말아야 합니다. 컴퓨터가 그렇게 만들어져있으니까요. 그리고 고정소수점 소수라는 방식도 있는데도 불구하고 부동소수점 숫자가 훨씬 더 많이 쓰이는 것은, 더 빠르게 더 큰 숫자를 계산할 수 있게 때문입니다.

이건 C/C++이나 델파이등 언어별로 나타나는 현상이라고 할 수 없는 것이, 다른 언어들에서도 일반적이기 때문입니다. 당장 찾아봐도 여러 사례가 나오네요. 자바도 그렇고,
http://mwultong.blogspot.com/2006/10/java-floating-point-97-90-06-04.html
PHP도 그렇고,
http://www.phpschool.com/gnuboard4/bbs/board.php?bo_table=tipntech&wr_id=61594&sca=%C1%A4%BA%B8&page=5
C#도 마찬가지고,
http://gongdo.tistory.com/318

물론 일부 언어나 개발툴 벤더에서 연산 결과값을 강제로 보정을 하는 경우도 있겠습니다만, 오히려 다른 문제를 만들수도 있기 때문에 부동소수점 연산의 오차는 언어 차원에서는 보정하지 않고 개발자들이 코드에서 상황별로 대처하는 것이 일반적인 원칙입니다.
주정섭 [jjsverylong]   2011-03-28 15:55 X
이 부동소수 연산 오차를 버그로 받아들일지 당연한 것으로 받아들일지는 각자의 판단이라고 쳐도, 과연 개발자 입장에서 어느 편이 편할까요?

금액이 큰 돈을 계산하다보면 이 오차가 때로는 아주 치명적이 됩니다.

델파이라면, 특히 Trunc, Frac, Int, Round 등을 호출할때 빈번해집니다. 재수가 좋으면 이 버그에 안 시달릴수도 있습니다. 빈도는 적지만 전혀 의외의 경우에 이 오차가 발생하는데, 일단 발생하면 일반적인 단순 형변환으로는 해결 불가능합니다.

이런 실수오차 문제를 해결하려면, 큰 금액이나 유효자리수가 큰 수를 계산할때, 이런 실수 연산의 유효자리수 오차를 이해하고 자동으로 보정하는 라이브러리를 사용해야 합니다. 과거 터보파워 제품 중에도 이런 실수 연산 라이브러리가 있고, 공개된 버전의 라이브러리도 있습니다.

그런데, 내 생각으로는 컴파일러 차원에서 이런 뻔한 실수 연산의 유효자리수 오차를 보정해 준다면 개발자 관점에서는 졸라 편하다고 봅니다. 나의 프로그램에서도 큰 금액인 경우, 무조건 이 실수연산 오차 보정 라이브러리 함수를 호출합니다만, 이거 졸라 번거롭습니다.
박지훈.임프 [cbuilder]   2011-03-28 16:14 X
물론 개발자 단위의 선호가 있을 수 있지만, 금액같이 정확성이 대단히 중요한 단위에는 가급적이면 부동소수점 값은 쓰지 않는 것이 원칙인데요. 특히 주정섭님은 델파이 개발자이시니 정말 웬만해서는 금액 연산을 위해 부동소수점을 쓰실 일이 없지 않습니까? currency를 쓰면 될 일을. 금액 값은 같은 종류의 금액 값들 사이에선 곱하기나 나누기 등 복잡한 연산보다는 가감 등 간단한 연산 위주이니까 피할 수만 있다면 굳이 부동소수점을 쓸 하등의 이유가 없습니다.

또 C/C++이라고 해도, 금액이라면 int64 등의 타입을 사용하고 소숫점을 따로 취급한 후 나중에 최종 결과 조회시에만 소숫점 처리를 할 수도 있습니다. 방법은 얼마든지 있죠. 금액은 일반적인 부동소수점들처럼 소숫점 밑으로 한정없이 내려가지도 않고 최고값이 한정없이 올라가지도 않는데, 다른 방법이 있기만 하다면 부동소수점을 쓸 이유가 있겠습니까?
심성현 [sim51177]   2011-03-28 16:28 X
int max = 1000;
int dif = max - 998;
int x = int(1000 * dif / 1000.0);
printf("%d\n", x);

이 코드를 여러가지 컴파일러로 컴파일 해보았습니다.

vc6 : 2
vc2009 : 2
vc2010 : 2
gcc(mingw) : 2
digital mars c++ : 2

free borland c++ 5 : 1
cbuilder 6 : 1
cbuilder 2010 : 1
cbuilder xe : 1

어떤게 표준에 맞는지는 모르겠지만 사람의 직관도 그렇고 다른컴파일러 모두 2 라고 하는데 홀로 1 이라고 하는건 좀 문제가 있는것 같습니다.
물론 문제가 될수 있는 코드를 작성하지 않는게 우선이긴 하겠지만요.
주정섭 [jjsverylong]   2011-03-28 16:31 X
만일 커런시 타입으로 이문제가 그리 쉽게 해결될 수 있었다면, 터보파워사에서는 뭐 때문에 그런 실수 연산 라이브러리를 만들었겠습니까? 내가 사용하는 공개용 실수 연산 라이브러리 제작자도 상당한 실력자인것은 분명한데, currency 타입을 몰라서 그런 복잡한 라이브러리를 만들었을까요?

나도 저 문제에 봉착했을때 currency도 동원하고 벼라별것을 다 해봤지만, 해결 불가능이었습니다. 이 문제의 근본적 문제는...

1.99999999999... 같은 무리수에 대해서 Trunc를 호출했을 때 이 값이 분명히 2로 반환되어야 하는데, 1로 반환되는 경우입니다. 그래서 유효자리수를 감안해서 실수 오차 보정이 필요한 것입니다. 재수가 좋으면 이런 오차에 봉착하지 않지만, 일단 이런 애매모호한 경우를 당하면 일반적 형변환 방법으로는 속수무책입니다.

연산의 결과가 유리수인 경우, 즉 소수이하가 딱 떨어지는 일반적 정수금액 X 정수수량 인 연산에는 이 오차 발생확률 거의 없습니다. 그러나, 실수금액 X 실수 수량인 경우에는 전혀 상황이 달라지고, 커런시 타입은 소수 이하 4자리 까지가 한계이기 때문에 이런 연산에서 제한될수 밖에 없습니다.
박지훈.임프 [cbuilder]   2011-03-28 16:44 X
대부분의 금액 계산에 실수 연산 라이브러리를 쓸 필요가 없는데 왜 쓰죠. 아무리 부동소수점 연산이 빨라져도 정수 연산이나 currency같은 고정소수점 타입보다는 느리고 부정확한데요. 또 말씀하신 대로라면 부동소수점 연산 라이브러리의 신뢰도는 절대적이어야 할텐데, 그렇다면 언어 차원이 아니라 CPU 차원에서 지원되어야 마땅하지 않겠습니까?

심성현님, 물론 그런 개발툴들 사이의 차이가 개발자의 편의와 관계가 있을 수는 있습니다. 하지만 앞에서 썼다시피 그게 더 큰 잠재적인 오류를 만들어낼 수도 있습니다. 그래서 컴퓨터의 기본 속성을 벗어나는 보정을 언어 표준으로 하지 않는 겁니다. 컴파일러가 지레짐작으로 보정하는 것이 사람의 머릿속의 지레짐작과 완벽하게 일치할 수가 없기 때문에 예상치 못했던 오류의 가능성이 생기는데, 이런 예상치 못한 오류는 예상할 수 있는 논리적인 오류보다 훨씬 더 심각하죠.
김상면 [windyboy]   2011-03-28 16:55 X
저도 임프님의 말씀에 전적으로 동의 합니다.
앞으로 열성팬이 되겠습니다.
그럼
박영목.월천 [gsbsoft]   2011-03-28 17:31 X
  빌더에서 실수 보정 방법...   자유게시판에 찾아보시면... 제가 질문한 것 댓글 엄청 달린 것
  찾을 수 있을 것입니다. 참고하실 분 참고하십시오....

  int max = 1000;
  int dif = max - 998;

  int x = StrToInt( FloatToStr(1000 * dif / 1000.0) );

  ShowMessage( x );   //VC 처럼 2가 나옵니다.  

  저도 이것 때문에 고생했습니다.  한번씩 실수를 사용하니... 몇년 지나면 잊어버리고 또 실수... ㅋㅋㅋ
김태선 [cppbuilder]   2011-03-28 18:22 X
8비트 시절부터 이런문제는 이미 익숙해져 있어
신경도 안쓰는 1인.
nansama [nansama]   2011-03-28 18:24 X
이문제는 보정을 해서 그렇다기 보다는 컴파일러가 SSE2 명령을 사용하지 않아서 발생하는것 같습니다.
SSE2 를 사용하면 VC처럼 작동 하는것 같습니다.
(참고) http://en.wikipedia.org/wiki/SSE2
Lyn [tohnokanna]   2011-03-28 23:36 X
동감...
에초에 제대로 짜면 아무 문제 없을일.

그리고 정말 중요한 계산에 부동소수점으로 계산하는것도 미친일.
심성현 [sim51177]   2011-03-29 01:24 X
헉 다시 테스트해보니
빌더xe 에서는 2가 나오네요.
아까 착각한건지...

빌더2010 과 빌더xe에서 결과가 다르게 나오다니. 이거 어디에다 장단을 맞추어야 할지..
디스어셈블리를 코드를 봐도 차이는 없는것 같은데.
fistp 명령어만 넘어가면 두개의 값이 달라지니..

어셈공부 해야겠당..
끄아악~
남병철.레조 [lezo]   2011-03-29 09:47 X
두둥~
nansama [nansama]   2011-03-29 14:02 X
예전버전은 fistp 직전에 fpu control word를 0x0C01 로 마스크를 설정 했습니다.
이건 부동소수를 정수처리 할때 Truncate 하라는 뜻입니다.
근데 xe버전에서 사사오입으로 처리 했다는건 음.. 이해 할수 없내요, 호환성을 위해라도 그렇게 만들지 않을것 같은데
xe버전이 없어서 테스트를 못해보겠내요,
아제나 [azena]   2011-03-29 17:36 X
문제 파악을 잘못 하셨다고 봅니다.
애초에 문제가 된 것은 부동소수점이 아니라고 보거든요.

문제가 된 코드를 다시 리뷰해보면,

    int n1, n2, n3, n4;
    double d;

    const int c = 2;
    int n = 2;

    n1 = (int)(1000 * 2 / 1000.0 );
    n2 = (int)(1000 * c / 1000.0 );
    n3 = (int)(1000 * n / 1000.0 );
    n4 = (int)(100 * n / 100.0 );

    d = 1000 * n / 1000.0;

    printf("%d, %d, %d, %d, %.0lf", n1, n2, n3, n4 ,d );

단지 부동소수점의 문제라면 이 코드의 결과가

1, 1, 1, 1, 2

이렇게 나와야 합니다.
그런데 빌더의 결과는

2, 2, 1, 2, 2

이렇게 나오니까요.

언어 차원에서 보정하지 않는게 좋다라고 하셨는데
빌더의 현재 상태는 반은 보정하고 반은 보정하지 않는 어정쩡한 상태라서
n2 과 n3 를 == 연산을 하면 false가 뜨는 기가막히는 상황을 맞을테니까요.

빌더 XE에서는 해결이 되었다니 더 논할 필요는 없다고 봅니다만
문제점은 인식하고 넘어가야겠죠.
심성현 [sim51177]   2011-03-29 21:56 X
대략난감합니다.
다른 컴퓨터에 깔린 빌더XE에서 테스트 해보니깐 이번엔 또 1이 나오네요.
맨처음에 XE에서 테스트할때 1이 나왔던게 착각이 아니었네요.
박우성 [solgari]   2011-03-31 10:04 X
김태선님이나 박지훈 님의 글은 저는 충분히 이해하고, 개인적으로 그 내용이 맞다라고 생각합니다.
하지만, 아제나님이 언급하신 것처럼, C++ Builder에서는 일관성이 없다는 것이 개발자에게 혼란을 주는 것입니다.

제생각에는 컴파일 시간에 계산되는 것은 보정이 되고, 실행시간에 계산되는 것은 보정을 하지 않는 것 같습니다.

이 부분에 대해 고수이신 김태선님이나 박지훈님의 의견도 듣고 싶습니다.

저는 C++ Builder 로 99%정도 개발하고, VC++로 1%정도 개발합니다. Turbo-C 때부터 사용했는데, 정말 좋은 C++ 개발 툴이라고 생각합니다. 델파이 때문에 C++ Builder는 조금 찬밥이 된 것은 아쉽지만, 엔바카데로에서 인수한 후 많이 좋아지고 있는 것도 사실인 것 같습니다. 

제 생각으로는 C++ Builder가 델파이와 달리 좀더 C++에 특화된 개발툴로 발전되길 희망합니다. 솔직히 지금은 델파이의 C++버전정도로 보여집니다.
김태선 [cppbuilder]   2011-03-31 17:44 X
아제나님이 일관성이 없다고 하셨는데, 그렇지 않습니다.

    const int c = 2;
    int n = 2;

    n1 = (int)(1000 * 2 / 1000.0 );
    n2 = (int)(1000 * c / 1000.0 );
    n3 = (int)(1000 * n / 1000.0 );
    n4 = (int)(100 * n / 100.0 );

에서 c는 상수이므로, 컴파일 할때 컴파일러가 계산한 결과 값을 대입하고
n 은 변수이므로 계산하는 코드를 컴파일러가 만들어 냅니다.
그러닌까, c 가 들어간 계산은

    n1 = (int)(2);
    n2 = (int)(2);

완전히 같은 문장입니다.
이건 아주 오래던부터 있어온 컴파일러의 기본이고
어느 컴파일러 할것 없이 동일 합니다.

빌더는 매우 일관성 있고 전통적으로 ansi 표준을 준수율이 VC 보다 높아 왔습니다.
최근 에디션은 어떤지 모르겠지만.

nansama [nansama]   2011-04-01 09:33 X
조금 다르게 말하면
델파이나 C++빌더의 컴파일러가 sse2같은 10년전 나온 cpu 기술이 (그것이 호환성 때문이었든) 컴파일러에 반영이 안된겁니다.
부동 소수는 원래 오차를 가지고 있지만 fpu는 80비트로 처리 하는데 컴파일러는 중간 과정을 64비트 더블에 넣어서
손실이 발생 합니다. 더구나 sse2는 128비트 이고 손실을 막는 여러기능이 있어서 굉장히 높은 정밀도를 제공 합니다.
이부분은 델파이나 빌더의 64비트 버전에서 좀더 심각 해지는데 64비트 버전에서는 커널모드에서 80비트 fpu레지스터를
사용할수 없고 사용자 모드에서도 콘텍스트 스위칭(스레드전환)시 이전 fpu 상태값을 보존하지 않기 때문에
os가 저장과 복원을 반복해야 하는등 여러가지 문제점이 있어서 64비트 버전에서는 아마도 sse2를 사용해야 할 상황입니다.
따라서 확신할 순 없지만 64비트 버전에서는 지금의 VC와 유사한 연산 결과가 나올것이며 기존 32비트 버전과는 다른
부동소수 연산결과가 예상됩니다. 아니면 뭐 성능이고 뭐고 귀차니즘과 호환성 때문에 80비트 fpu만 끝까지
고집 할수도 있겠네요,... 어떻게 될지 제품이 나와바야 알겠지만 ...

+ -

관련 글 리스트
19292 부동소수점 계산 관련 논란에 대해 박지훈.임프 11581 2011/03/28
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.