진법, 바이트 크기, 그리고 사람들이 다투는 단위들
적어놓은 숫자는 두 가지를 함께 담고 있습니다. 값, 그리고 그 값을 어떻게 읽어야 하는지에 대한 전제들입니다. 0xFF, 255, 0o377, 11111111은 하나의 양을 네 가지 표기로 쓴 것입니다. 테라바이트와 테비바이트는 10퍼센트 차이가 납니다. 1,234는 런던에서는 대략 천이고 베를린에서는 대략 일입니다. 숫자와 관련된 버그의 대부분은 산술이 아니라 전제에서 나옵니다.
2진법, 8진법, 10진법, 16진법
모든 자리값 진법은 같은 방식으로 동작합니다. 각 자리는 오른쪽 자리의 밑수 배이고, 숫자는 0부터 밑수보다 하나 작은 값까지 씁니다. 16진법은 여섯 개의 숫자가 더 필요하므로 10부터 15까지를 A부터 F로 빌려 씁니다.
16진법을 바이트와 색에 쓰는 이유는 16이 2의 4제곱이기 때문입니다. 그래서 16진수 한 자리가 정확히 4비트이고 한 바이트가 정확히 16진수 두 자리이며, 그 사이에 자리 올림이 없습니다.
0 0000 4 0100 8 1000 C 1100
1 0001 5 0101 9 1001 D 1101
2 0010 6 0110 A 1010 E 1110
3 0011 7 0111 B 1011 F 1111
이 표가 변환의 전부입니다. 16진수를 2진수로 바꾸려면 각 자리를 해당하는 4비트로 바꿔 놓으면 됩니다. 0x2F는 0010 1111이 됩니다. 되돌아올 때는 비트를 오른쪽에서부터 네 개씩 묶고 왼쪽은 0으로 채운 다음, 각 묶음을 표에서 읽어내면 됩니다.
10진수는 자리값을 쓰면 됩니다. 0x2F는 2 x 16 + 15이므로 47입니다. 반대 방향으로는 16으로 계속 나누면서 나머지를 아래에서 위로 읽습니다.
200 / 16 = 12 remainder 8 -> 8
12 / 16 = 0 remainder 12 -> C
200 decimal = 0xC8
8진법은 한 자리에 3비트를 담으며, Unix 권한을 755처럼 쓰는 이유가 이것입니다. 각 자리가 rwx 세 칸에 대응하며 7은 111, 5는 101입니다. #FF8800 같은 CSS 색은 채널당 하나씩 3바이트이고 각 값은 0부터 255까지이며, 세 자리 축약형 #F80은 0으로 채우는 것이 아니라 각 자리를 두 번 반복해 펼칩니다. 진법 변환기는 네 진법을 한꺼번에 다룹니다.
2의 보수, 그리고 범위가 정해지는 방식
한 바이트는 256가지 서로 다른 비트 패턴을 담습니다. 부호 없는 값으로 읽으면 0부터 255까지입니다. 부호 있는 값으로 읽으면 맨 위 비트가 음수 절반을 표시하고, 음수 값은 그 패턴에서 256을 뺀 값이 됩니다. 이 방식이 2의 보수이며, 이렇게 하면 일반적인 2진 덧셈이 부호 있는 값과 부호 없는 값 모두에서 똑같이 동작하기 때문에 채택되었습니다.
어떤 수의 부호를 뒤집으려면 모든 비트를 반전하고 1을 더합니다. 범위가 비대칭인 이유는 0이 음수가 아닌 절반의 128개 자리 중 하나를 차지하기 때문이며, 그래서 부호 있는 바이트는 -128부터 127까지입니다. 범위의 꼭대기에 1을 더하면 바닥으로 감깁니다.
0111 1111 127
+ 0000 0001 1
= 1000 0000 -128
대부분의 언어에서는 아무 경고도 뜨지 않습니다. 값이 그냥 범위의 반대편에 나타날 뿐입니다. 32비트에서 일어나는 같은 순환이 2038년 문제이며, 부호 있는 32비트 Unix 타임스탬프가 2,147,483,647초에서 바닥납니다. 부호 없는 16비트는 0부터 65,535까지인데, TCP 포트 번호가 거기에서 멈추는 이유가 바로 이것이고, 무작위 포트 생성기가 1024 미만의 예약된 포트를 피하면서 그 범위에서 값을 뽑는 이유도 같습니다.
킬로바이트, 키비바이트, 그리고 사라진 기가바이트
저장 장치 제조사는 10의 거듭제곱으로 셉니다. 운영체제는 오랫동안 2의 거듭제곱으로 세면서 그 결과에 10진 접두어를 붙여 왔고, 논쟁은 여기에서 시작됩니다. 2진 접두어(KiB, MiB, GiB)는 그 모호함을 없애기 위해 존재합니다.
| 10의 거듭제곱 | 10진 값 | 2진 접두어 | 2진 값 | 차이 |
|---|---|---|---|---|
| kB, 10^3 | 1,000 | KiB, 2^10 | 1,024 | 2.4% |
| MB, 10^6 | 1,000,000 | MiB, 2^20 | 1,048,576 | 4.9% |
| GB, 10^9 | 1,000,000,000 | GiB, 2^30 | 1,073,741,824 | 7.4% |
| TB, 10^12 | 1,000,000,000,000 | TiB, 2^40 | 1,099,511,627,776 | 10.0% |
| PB, 10^15 | 10^15 | PiB, 2^50 | 1,125,899,906,842,624 | 12.6% |
1 TB 드라이브는 판매 기준으로 10^12바이트를 담습니다. 그 값을 2^30으로 나누면 931.32가 나오므로, Windows는 2진 값에 10진 이름표를 붙여 약 931 GB라고 보고합니다. 사라진 것은 없습니다. 같은 바이트 수를 다른 수로 나누고 있을 뿐입니다. macOS와 대부분의 Linux 도구는 이제 10진 GB로 보고하며, 이는 제품 상자에 적힌 값과 일치합니다. 메모리 크기는 실제로 2진 단위이므로 16 GB RAM은 정말로 16 GiB입니다.
네트워크 속도는 바이트가 아니라 비트로 세며, 언제나 10진입니다. 100 Mbps 회선은 초당 1억 비트를 실어 나르므로 오버헤드를 빼기 전에는 초당 12.5 MB입니다. 프로토콜 오버헤드가 몇 퍼센트를 가져가므로 현실적인 상한은 초당 11에서 12 MB입니다. 광고에 적힌 메가비트 수치를 8로 나누는 것이 빠른 확인법이며, 저장 단위는 단위 변환기가 다룹니다.
부동소수점, 그리고 돈이 부동소수점이 아닌 이유
2진 소수는 2분의 1, 4분의 1, 8분의 1 등의 합만 표현할 수 있습니다. 10진법에서 3분의 1을 정확히 쓸 수 없는 것과 같은 방식으로 10분의 1에는 정확한 2진 표현이 없으므로, IEEE 754 배정밀도로 저장된 0.1은 10분의 1보다 아주 조금 큽니다. 그런 근삿값 두 개를 더하면 오차가 드러납니다.
0.1 + 0.2 = 0.30000000000000004
배정밀도 값의 가수는 53비트입니다. 그래서 2^53(9,007,199,254,740,992)까지의 모든 정수와, 분모가 2의 거듭제곱인 모든 분수가 정확히 표현됩니다. 2^53을 넘어가면 표현 가능한 값 사이의 간격이 2가 되므로 2^53 + 1은 2^53과 같아집니다. JSON으로 전달되는 64비트 데이터베이스 식별자를 문자열로 보내야 하는 이유가 이것입니다. JavaScript의 숫자는 배정밀도 값이고, 19자리 ID는 조용히 마지막 자릿수를 잃습니다.
돈은 정수 형태의 보조 단위(펜스, 센트)로 저장하거나 10진법으로 동작하는 십진 타입을 쓰십시오. 부동소수점 값을 등호로 비교하지 말고, 차이를 작은 허용 오차와 비교하십시오. 수식 계산기도 배정밀도 값으로 동작하므로 같은 한계를 그대로 물려받습니다.
반올림, 그리고 두 번 반올림하기
올림 방식은 딱 절반인 값을 0에서 먼 쪽으로 보냅니다. 짝수 반올림, 즉 은행가 반올림은 딱 절반인 값을 가장 가까운 짝수 자리로 보내는데, 이렇게 하면 긴 숫자 열에서 올림 방식이 만들어내는 위쪽 편향이 사라집니다. 이것이 IEEE 754의 기본값이며 여러 회계 기준의 규칙이기도 합니다.
| 값 | 올림 방식 | 짝수 반올림 |
|---|---|---|
| 0.5 | 1 | 0 |
| 1.5 | 2 | 2 |
| 2.5 | 3 | 2 |
| 3.5 | 4 | 4 |
| -0.5 | -1 | 0 |
반올림은 원래 값에서 한 번만 하십시오. 2.4449를 소수점 셋째 자리로 반올림하면 2.445가 되고 그것을 다시 둘째 자리로 반올림하면 2.45가 되지만, 원래 값을 곧바로 둘째 자리로 반올림하면 2.44가 됩니다. 중간 열들을 거치며 연쇄적으로 반올림하는 것은 합계가 1페니씩 어긋나는 흔한 원인입니다. 관련된 놀라움이 하나 더 있습니다. 2.675는 대부분의 언어에서 2.67로 반올림되는데, 저장된 배정밀도 값이 2.67499999999999982이기 때문입니다.
퍼센트와 퍼센트포인트
비율이 4%에서 5%로 움직였다면 1퍼센트포인트 올랐거나, 상대적으로는 25% 오른 것입니다. 둘 다 맞으며, 어떤 단위인지 반드시 밝혀야 합니다.
백분율 변화는 서로 상쇄되지 않습니다. 50% 하락한 뒤 50% 상승하면 100이 아니라 75가 남습니다. 두 번째 백분율이 더 작아진 기준값에서 계산되기 때문입니다. p만큼의 하락을 회복하려면 p / (1 - p)만큼의 상승이 필요하므로, 50% 하락에는 100%가, 80% 하락에는 400%가 필요합니다. 연속된 변화는 합이 아니라 곱으로 쌓여 등비수열을 이루며, 수열 생성기가 모델링하는 성장이 바로 그것입니다.
옛 값에서 새 값으로의 변화는 (new - old) / old x 100이며, 백분율 계산기가 차이로 보고하는 값이 이것입니다. 20% 부가가치세를 빼려면 20%를 빼는 것이 아니라 1.2로 나눠야 합니다. 크기가 다른 집단들의 백분율을 평균하면 가중 평균을 쓰지 않는 한 전체 수치가 틀리게 나오며, 합계와 평균 도구가 평균 옆에 중앙값을 함께 보여주면 그 치우침이 눈에 보입니다.
로캘에 맞춰 서식을 입힌 숫자
서식은 표시에 관한 결정이며, 로캘을 모르면 되돌릴 수 없습니다. 1.234는 영국에서는 1을 조금 넘는 값이고 독일에서는 천입니다. 1,234는 그 반대입니다. 어떤 로캘은 공백이나 아포스트로피로 자릿수를 묶습니다.
| 로캘 | 1234567.89 |
|---|---|
| en-GB, en-US | 1,234,567.89 |
| de-DE | 1.234.567,89 |
| fr-FR | 1 234 567,89 |
| de-CH | 1'234'567.89 |
| en-IN | 12,34,567.89 |
인도식 묶음은 첫 구분자를 세 자리 뒤에 놓고 그다음부터는 두 자리마다 놓아서, 천과 백만이 아니라 라크와 크로르 단위를 만듭니다. 서식이 입혀진 문자열을 다시 숫자로 파싱하는 일이 손실을 일으키는 이유가 정확히 이것입니다. 그러니 값은 서식 없이 소수점을 점으로 써서 저장하고 전송하며, 서식은 표시하는 지점에서만 입히십시오. 숫자가 읽는 사람에게 모호하지 않아야 하는 자리에서는 숫자 문자 변환 도구로 풀어 쓰는 것이 수표나 계약서에서 쓰는 일반적인 방식입니다. 로마 숫자는 이 모든 이야기 바깥에 있습니다. 자리값도 없고 0도 없으므로, 로마 숫자 변환기가 다루는 것은 진법이 아니라 눈금 세기입니다.