실수의 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번째 자리에서 반올림하여 표현
-> 정확성보다 속도에 초점을 맞춘 듯