float 에서 0.0001 같은 수를 반복해서 더했을때 오류 잡는 방법

ssehoony의 이미지

아래 소스를 gcc version 3.3.4 에서 컴파일해서 실행해 보면
99.3273 이 나옵니다.
float 의 내부구조상 그렇게 될 수 밖에 없는 듯 한데요.
float 의 어떤 특징 때문에 계산 값이 99.3273이 나오는지도 궁금하고
이걸 해결할 방법은 없습니다.

아무리 생각해보 float 의 유효 자리수를 넘지 않았으니깐 이상없을 듯 한데
거참...

#include <iostream>

using namespace std;

int main()
{
    float f = 0.0;

    for(int i = 0; i < 1000000; ++i)
        f += 0.0001;

    cout << f;

    return 0;
}
익명 사용자의 이미지

f에 더하는 값을 0.5 나 0.25 처럼.
2의 -n승으로 하시면 딱 맞아 떨어지는 값이 나올겁니다.
거꾸로 0.0001을 2의 -n승으로 표현해보시면 9.99999975e-05 정도가 나오나 봅니다.

crimsoncream의 이미지

Anonymous wrote:
f에 더하는 값을 0.5 나 0.25 처럼.
2의 -n승으로 하시면 딱 맞아 떨어지는 값이 나올겁니다.
거꾸로 0.0001을 2의 -n승으로 표현해보시면 9.99999975e-05 정도가 나오나 봅니다.

로그인을 안하고 썼더니 수정도 안되는 군요 :(
그러니까 0.0001 * 2는 실제로는 9.99999975e-05 * 2가 되어서 0.000199999995 정도가 되고.. 계속 쌓이면 결국 말씀하신 값이 나올 것 같습니다.

오늘 우리는 동지를 땅에 묻었습니다. 그러나 땅은 이제 우리들의 것입니다.
아직도 우리의 적은 강합니다. 그러나 우리는 그들보다 많습니다.
항상 많을 것입니다.

lsj0713의 이미지

컴퓨터는 내부적으로 수를 2진법으로 표현합니다. 그래서 0.0001 같은 수를 정확히 표현할 수 없습니다. 자세한 것은 컴퓨터에서 내부적으로 부동소수점을 표현하는 방법중의 하나인 IEEE 754에 대해 검색해 보십시오.

http://babbage.cs.qc.edu/courses/cs341/IEEE-754references.html

float 대신에 double을 사용하면 좀 더 정확하게 값을 표현할 수 있습니다. 그렇다고 해도 완벽히 정확하진 않지만 어쨌든 float 보다는 낫습니다. 제가 시험해본 결과로는 제대로 100이 찍히는군요.

참고로 C에서는 float형에 대한 자세한 정보를 float.h에 상수값으로 정의해 두고 있습니다. 또한 가장 최근의 표준인 C99에서는 내부적으로 2진 표기법을 사용하는 환경에서 소수점 상수의 정확한 표현을 위해서(...라고 저는 추측하지만, 다른 이유 때문일지도 모르겠습니다-_-;;) 0x1d.8fP3 과 같은 형식의 16진 소수점 상수 표현 역시 지원하고 있습니다. 10진수 0.001을 제대로 표현할 수 없는 것은 마찬가지겠지만, 0.5 혹은 0.75와 같은 수에 대해서는 좀 더 정확하게 오해의 여지 없이 표기할 수 있겠지요.

gyxor의 이미지

저도 위 사실이 궁금해서 .. 정확한 원인을 찾아봤습니다.

0.0001 을 float로 표현하면
0.0001 =>0 01110001 1.10100011011011100010111
입니다.
지수부분이 113 이므로 -127을 하면 -14이고
1.10100011011011100010111 * 2^(-14)입니다.
풀어서 쓰자면
0.0000000000000110100011011011100010111 ...
이렇게 표현이 되는데요..
뒷부분에 ... 이 있는 이유는 0.0001을 가지고서 정확한 2진수를 구하려면.. 계산기로 23비트 넘게 해봤지만 끝을 못찾았습니다.
23비트보다 더 큰 비트가 있어야 정확히 표현할 수 있다는것인데요
IEEE754에 의해서 가수부분이 23비트
가 한계입니다. 따라서 뒷부분 ... 부분이 날아가 버리게 되고
값은 실제보다 작아집니다.
위의 값을 10진수로 바꿔봤습니다.
계산과정입니다.
14> 0.00006103515625
15> 0.000030517578125
17> 0.00000762939453125
21> 0.000000476837158203125
22> 0.0000002384185791015625
24> 0.000000059604644775390625
25> 0.0000000298023223876953125
27> 0.000000007450580596923828125
28> 0.0000000037252902984619140625
29> 0.00000000186264514923095703125
33> 0.000000000116415321826934814453125
35> 0.00000000002910383045673370361328125
36> 0.000000000014551915228366851806640625 =1.4551915228366851806640625e-11
37> 0.0000000000072759576141834259033203125 =7.2759576141834259033203125e-12

결과 = >0.000099999997473787516355514525
(물론 이 값 또한 36과 37번째 비트 계산중 계산기의 한계로
오차가 포함이 되었으므로 컴퓨터에서 인식하는 값보다 조금더
작겠습니다.)

결국 정확한 0.0001 이 아니라 그보다 조금 작은 수가 저장이 됩니다. 따라서 최종결과가 작게 나오는것입니다.
0.99999 를 계속 반복해서 더해보시면 이해가 될것입니다.
다시말해
0.99999 + 0.00001 = 1이지만
0.00001을 10만번 더하면 오차부분이 계산과정에서 계속 누적이
되기때문에 1보다 작은 값이 나오는것입니다.
그런데 반대로
실제로는 결과값이 더 큰 값이 나오는 경우가 있었습니다.

#include <iostream> 
#include <fstream>
#include <iomanip>
using namespace std; 

int main() 
{ 
    int b[8] = {0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01};
    float sum = 0.0; 
	unsigned char *p = (unsigned char *)&sum; 
	ofstream fs("test.txt",ios::out);
    
	for(int i = 0; i < 10000; ++i) 
	{
		sum += 0.0001f; 
        fs << left << setw(10)<< sum << "=>";

		for(int m = 3; m >= 0 ; m--){
		    for(int n = 0; n < 8; n++){
		       if(  (p[m] & b[n]) !=0) 
			   {
				   fs << "1";
			   }else fs << "0";
			   if((m==3 && n==0)) fs << " ";
			   if((m==2 && n==0)) fs << " 1.";
			}
		}
	fs << endl;
	}
	
    fs << "E N D";
	cout << 0.99999 + 0.00001;
    return 0; 
} 

위 소스를 돌려보면 0.00001을 10만번 더하는 과정이
test.txt 파일로 생성이 됩니다.
정확한 비트구조와 함께 값이 보여지는데요..
그것을 분석한결과 원인을 찾아냈습니다.

더하기가 진행되는 중간 쯔음에..이상한 부분이 있었습니다.
0.0368 + 0.0001 은 0.0369가 되어야 하는데
0.0369001 이 되는것이었습니다.
0.0368 도 실제값보다 미세하게 작을것이고 0.0001도 실제보다
작기 때문에 0.0369 보다 큰값이 나올 수 가 없다고 생각했습니다.
계산과정에서 비밀이 숨어있었습니다.

0.0368 =>0 01111010 1.00101101011101110100110
+
0.0001 =>0 01110001 1.10100011011011100010111

0.0000100101101011101110100110
+) 0.0000000000000110100011011011100010111
__________________________________________
0.0000100101110010010010000001100010111

0.0369001
=>0 01111010
1.00101110010010010000010

0.0369001의 실제 비트구조와 계산된 결과값을 비교해보면
계산결과값의 마지막 23비트의 바로앞 24비트에서 비트가 1인
경우에 반올림이 되고 있었습니다.
그래서
0.0369001의 가수부분이
1.00101110010010010000001 이 아니라
1.00101110010010010000010 으로 되었습니다.

0.0427001과 0.0001을 더하는 경우에도.. 발생합니다.

0.0427001
=>0 01111010 1.01011101110011001011010
+
0.0001
=>0 01110001 1.10100011011011100010111

0.0000101011101110011001011010
+)0.0000000000000110100011011011100010111
_________________________________________
0.0000101011110100111100110101100010111

0.0428002 =>0 01111010
1.01011110100111100110110

이렇게 반올림에 의해 더해지는 값은
10진수로 계산하면 2의 -28승인
0.0000000037252902984619140625 인데요..
비트수의 한계로 발생한
0.0001의 오차 포함값
0.000099999997473787516355514525 의 부족한
오차부분을 훨씬 웃도는 숫자입니다.

위의 두가지 예에서는 실제로 값이 달라졌기 때문에 보여드린것
이구요..(test.txt 참조) 10만번의 단계중에서 반올림 상황은 언제고 발생할 수 있습니다.

따라서 이러한 반올림! 이 누적이 된다면 덧셈과정에서 누적된
마이너스 오차부분을 다 채우게 되고 오히려 플러스 오차가
생기게 되는것입니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.