C++ 반올림, round함수 만들기
컴퓨터는 단지 숫자다 :: 3 .round 함수 만들기
이번 주제 :: round함수 구현하기
- 우리는 0과 1로 정수와 소수를 표현하는 방법을 배웠고 부동 소수는 오차를 가지기 때문에 0.499999999와 같은 값을 반올림 하게되면 0이 아닌 1이 나올 수도 있다는 사실도 알았다. Round, 혹은 ceil, floor함수는 이를 어떻게 해결할까?
서론
반올림 함수는 어떻게 작성되는가. 여기에 대해서는 다루고 있는 글들이 거의 없었기 때문에 한번 다뤄보려고 한다. 우리가 말하는 반올림 방법과 수의 반올림 파트에서 다룬 To nearest, Ties-to-even과는 근소한 차이이기 때문에 우리에게 익숙한 반올림 함수를 예를 들어 설명하려고 한다.
처음으로 round 함수를 작성해 보라고 하거나 round 함수 구현에 대해 스택오버 플로우를 찾아보면 부동 소수점에 반올림 함수를 다음과 같이 작성하기 쉽다.
// C++
float myround(float x) {
return floor(x + 0.5);
}
만약 ceil이나 floor 함수를 주지 않고 다시 round함수를 구현해보라고 한다면 충분히 짧은 시간에 동작하도록 짤 수 있을까?
또한 위의 코드는 정수에서는 잘 동작하겠지만 0.499999994를 집어 넣었을 때에는 0이 아닌 1을 반환한다. 이는 0.499999994를 표현하는 부동소수값이 정확히 0.499999994이 될 수 없는 부동소수점 구조상의 문제이다. 하지만 파이썬의 round함수나 C의 round함수는 위의 문제를 우아하게 0으로 출력한다.
Harder than it looks like :: 생각보단 어렵다
round함수를 Ceil이나 floor를 사용하지 않고 float에 대해서 구현하고 싶어 스택 오버 플로우를 참고해서 작성해보았다. 실제 c에서 제공하는 round함수 수준으로 작성하고 싶었지만 목표를 달성하지 못하였다.
0.4999는 제대로 동작했지만 원래 목표였던 0.49999999999999는 0이 아닌 1로 반올림 되었다. 이는 0.49999999999999가 float으로 함수에 들어가면서 부동소수점이 가지는 오차 때문에 0.5로 인식되는 문제라고 생각한다. 이는 부동 소수점 표기법으로 바뀌기 전에 유리수로 계산하거나 정수화해서 해결하는 방법밖에 없을 것 같은데 어셈블리어를 건드리지 않고 구현하는 방법이 떠오르지 않는다.
첫 번째 버전 소스코드는 다음과 같다. To-nearest, away-from-zero의 규칙을 따라 작성하였다.
#include <iostream>
#include <cmath>
using namespace std;
union
{
float input;
__uint32_t output;
} data;
void printBits(float x);
void GET_FLOAT_WORD(__uint32_t &a, float b);
void SET_FLOAT_WORD(float &a, __uint32_t b);
void printBits(float x);
void printBits(__uint32_t x);
float roundf(float x);
float ysRound(float x);
int main()
{
cout << ysRound(0.49999) << endl;
cout << ysRound(0.50001) << endl;
}
void printBits(float x)
{
bitset<32> bits(x);
cout << bits << endl;
}
void printBits(__uint32_t x)
{
bitset<32> bits(x);
cout << bits << endl;
}
void GET_FLOAT_WORD(__uint32_t &a, float b)
{
data.input = b;
a = data.output;
}
void SET_FLOAT_WORD(float &a, __uint32_t b)
{
data.output = b;
a = data.input;
}
float ysRound(float fl)
{
__uint32_t w;
GET_FLOAT_WORD(w, fl);
bitset<sizeof(__uint8_t ) * 8> bits(w);
int exponent = ((w & 0x7f800000) >> 23) - 127;
if(exponent < 23)
{
if(exponent < 0)
{
w &= 0x80000000;
if(exponent == -1)
{
w |= ((__uint32_t)127 << 23);
}
}
else
{
unsigned int exponent_mask = (0x7fffff >> exponent);
if((w & exponent_mask) == 0)
{
return fl;
}
w += (0x00400000 >> exponent); // + 0.5
w &= ~exponent_mask;
}
}
else
{
return fl;
}
SET_FLOAT_WORD(fl, w);
return fl;
}
이전 포스트에 공부한 부동 소수점을 알고 있어야 코드가 이해가 가능하다.
0.499999994의 수도 정확하게 round할 수 있는 두 번째 버전의 코드를 다음에 올리도록 하겠다.
bitset 자료형
- bitset은 bool array이다. 하나의 bool을 저장하는 데 1비트만 사용하기 때문에 char을 쓰는 것보다 경제적이다. 파라미터로 정수, float, string등을 입력할 수 있다.
__uint32_t
- unsigned int를 typedef를 사용해서 __uint32_t로 표기함으로써 32비트라는 정보를 명시해서 사용한다.
댓글
댓글 쓰기