실수의 2진수 표현

 

정수부

 

정수는 이진수로 2로 나누어 나머지로 표현 가능

 

소수부

소수부는 값이 딱 나누어 떨어지는 유한소수와

소수점 아래 셀 수 없는 무한소수로 나누어짐 

유한소수 무한소수

유한소수 같은경우는 컴퓨터에서 표현할 수 있는 반면, 무한소수는 2진수로 표현할 수 있는데 한계가 있다.

cout << ((1.0 == 1.0f) ? "true" : "false") << endl;      // true
cout << ((1.1 == 1.1f) ? "true" : "false") << endl;      // false
cout << ((0.625 == 0.625f) ? "true" : "false") << endl;  // true
cout << ((0.9 == 0.9f) ? "true" : "false") << endl;      // false  
cout << ((0.01 == 0.01f) ? "true" : "false") << endl;    // false

같은 숫자이지만 f가 붙지않은 소수점들은 기본 자료형 double로 저장되고, f가 붙은 소수점들은 float로 저장된다.

유한소수인 1.0 / 0.625 는 double / float 비트로 각각 표현하였을때 완전히 같지만, 나머지 무한소수들은 float가 중간에 끊기기 때문에 false를 나타내는 것이다.

무한소수의 예시

그래서 소수 타입의 자료형의 판별이 필요하다면 같은 자료형을 사용해서 비교해야 한다.

// 이미 float 된 값을 다시 double로 변환한다고 소수점이 채워지는게 아님
cout << ((1.1 == (double)(1.1f)) ? "true" : "false") << endl;   // false

cout << (((float)1.0 == 1.0f) ? "true" : "false") << endl;      // true
cout << (((float)1.1 == 1.1f) ? "true" : "false") << endl;      // true
cout << (((float)0.625 == 0.625f) ? "true" : "false") << endl;  // true
cout << (((float)0.9 == 0.9f) ? "true" : "false") << endl;      // true
cout << (((float)0.01 == 0.01f) ? "true" : "false") << endl;    // true

 

혹은 소수점 n번째 자리에서 반올림해서 비교할 수도있는데 C++ 에서는 첫째 자리에서 반올림 하는 함수밖에 없다.

double a = 1.000000000000001;
double b = 1.000000000000002;

factor = 10^15
round(a * factor); // 1000000000000001
round(b * factor); // 1000000000000002

물론 엄청 극한으로 가면 오류는 있긴하지만 거의 대부분의 상황에서는 이렇게까진 안할 듯

FMath::IsNearlyEqual(double A, double B, double Tolerance = SMALL_NUMBER)

언리얼 같은경우 A와 B의 차이가 Tolerance 값 이하라면 같다고 판별하는 함수를 사용하면 좋음 

 

 

 4bytes = 32bits 인 float 자료형을 예시로 컴퓨터에서 실수를 표현하는 방식으로는 두 가지 방식이 존재

 

고정 소수점 

정수를 표현하는 비트와 소수를 표현하는 비트를 미리 고정하고,  해당 비트만을 활용하여 실수를 표현

1비트 - 부호 (Sign)  양수 0 / 음수 (1)

15비트 - 정부수

16비트 - 소수부

 

ex) 215 - 1 = 32767 를 넘는 실수 표현 불가

ex) 0.3 -> 0.0100110011001100(2) 까지 표현 가능

 

이 경우엔 정수부가 모두 0 이고 순환소수가 중간에 끊겨 표현 범위가 제한되고, 낭비되는 공간이 많다. 

-215 ~ 215 - 1 범위 제한과 소수점의 제한의 문제를 해결하기위해 컴퓨터에선 부동 소수점 방식을 사용한다.

 

 

부동 소수점

정수와 소수 크기에 상관없이 정해진 비트 내에서 값을 표현

ex) 7.625 -> 111.101(2) 일 때 부동소수점 으로 표현하면 1.11101 X 22 로 표현한다.

 

여기서 bias 라는 값이 등장하는데 (32bit 에서 127) 

부호 없는 8bit 같은경우는 0 ~ 255 까지 표현이 가능하다.

하지만 지수부 n을 음수값을 표현하고 싶을 때 -128 ~ 127 까지 표현가능하다.

실제 지수 값 = 저장된 지수 값 - bias

 

IEEE 754 표준

저장된 8bit 지수부  실제 지수 = 저장된 지수 - bias 계산된 지수부 값
0000 0000 0 - 127 = -127 Zero (특수)
0111 1100   (124) 124 - 127 = -3 2-3
0111 1110   (126) 126 - 127 = -1 2-1
0111 1111    (127) 127 - 127 = 0 20
1000 0000   (128) 128 - 127 = 1 21
1000 0100   (132) 132 - 127 = 5  25
1111 1111    (255) 255 - 127 = 128 Inf or Nan (특수)

계산된 지수에 127 을 더하여 127보다 작으면 음수, 127 보다 크면 양수로 구분할 수 있도록 함 

 

지수부 값의미설명

0000 0000 Denormalized (비정규화 수) or 0 "매우 작은 수" 또는 "완전 0"
0000 0001 ~ 1111 1110 Normalized (정규화 수) 일반 숫자 표현
1111 1111 특수 값 (Infinity, NaN) 무한대 또는 Not a Number

 

지수부 에서는 부호비트를 사용하지 않는 이유?

값 = (-1)Sign * 1.(가수) * 2(지수)

부호비트의 경우는 값을 다 계산한 다음 양수냐 음수냐만 구분하는 1bit

 

지수부의 맨 앞을 부호비트로 사용할 시 

 

  • Bias 방식 → 그냥 빼면 됨 (32bit 통째로 비교 가능) 
  • 부호 방식 → 부호 보고 조건 처리 해야함 (같은 부호인지? 매번 mask / shift / xor 등의 처리가 필요함)

→ 하드웨어, 연산비용 증가
→ 정렬할 때 메모리 비교만으로 안됨

 

 

다시 돌아와서 예제를 들면 다음과 같이 표현 가능하다.

-24.625 ->  11000.101(2) = 1.1000101(2) * 24 

 

  부호비트 지수부 가수부
저장된 값 1 10000011   =  4 + 127 (bias)  10001010000000000000000
표현 식 (값) (-1)1 24 1.10001010000000000000000

 

 

C++에서 실수의 기본자료형은 double, 뒤에 f붙은것들은 float

float double 은 부동소수점 자료형

C++ 표준 에서는 고정 소수점 타입이 별도로 제공되진 않음

 

 

지수 표기법

10의 지수 대신 사용, 사람이 알아볼 수 있게 

ex)

-24.625 = -2.4625e+1

0.05 = 5e-2

 

float의 범위는 -3.4e-37 ~ 3.4e+38

 

지수의 범위

최소 지수 (0 은 특수) 1 - 127(bias) = 2-126  대략  1.17 X 10-38

최대 지수 (255는 특수) 254 - 127(bias) = 2127  대략  1.701 X 1038

 

가수의 범위

1.0 ~ 2.0

가수부가 전부 0 이면 1.00000..

가수부가 전부 1 이면 1.111111.. (2와 유사)

그래서 2배로 침

 

고로 float의 범위는 -3.4e+38 ~ 3.4e+38

(-1) X (가수 범위) X (지수 최대) ~ (가수 범위) X (지수 최대)

 

 

소수점 관련 - 블록체인 예시

비트코인은 소수점 8자리 까지 분할할 수 있게 설계되었다.

하지만 트랜잭션 발생(거래) 시, 가장 작은 단위인 사토시(sats)를 이용하여 단순한 정수 연산을 한다. 

0.00000001 BTC = 1 sats

1 BTC = 100,000,000 sats

 

이더리움

이더리움은 소수점 18자리 까지 분할 가능

가장 작은 단위인 Wei 존재, 마찬가지로 트랜잭션 발생 시, Wei를 이용한 정수 연산 

0.000000000000000001 Ether = 1 Wei

1 ETH = 1+e9 Gwei = 1+e18 wei

 

-> 표현의 한계가 있는 부동 소수점을 사용하지 않고 최대한 정수 연산을 사용 

 

 

 

왜 분수를 사용하지않고 데이터 손실이 있는 소수 타입을 사용하는가?

부동 소수점 같은경우 << , >> 의 비트 이동의 시프트 연산자는 연산이 빠름

분수의 더하기 뺴기 연산 시, 두 분모의 최소공배수를 찾아야 하고 곱셈 나눗셈 경우 분모 분자 모두 곱셉 나눗셈 연산이 들어가기 때문에 비교적 느림

 

또한 분자와 분모는 결국 값이 커질 것 이지만, 부동 소수점은 표현가능한 만큼 표현하고 나머진 반올림함

 

ex) 0.1을 32bit 부동소수점으로 변환과정에서 가수값 반올림

가수값: 1001100110011001100110011001... 에서

가수값: 10011001100110011001101 반올림된 값 

가수범위는 23자리까지 표현 가능하므로, 소수점 아래 24번째 자리에서 반올림하여 표현

 

-> 정확성보다 속도에 초점을 맞춘 듯

+ Recent posts