비트·바이트·주소
값의 폭과 주소의 폭을 구분하고, KiB와 kB처럼 기준이 다른 단위를 계산합니다. 바이트마다 붙는 주소와 여러 바이트 값이 차지하는 범위, 정렬, 같은 값을 바이트 순서만 달리 놓는 엔디언을 작은 메모리 표와 코드로 확인합니다.
"32비트 컴퓨터의 메모리는 32바이트인가?"라는 질문에 답하려면 먼저 그 32비트가 무엇의 크기인지 물어야 합니다. 컴퓨터에서 비트 수는 한 번에 다루는 값의 폭을 말하기도 하고, 메모리 위치를 가리키는 주소의 폭을 말하기도 하며, 한 번에 옮기는 통로의 폭을 말하기도 합니다. 이 글에서는 저장 단위와 주소를 구분하고, 여러 바이트로 된 값이 메모리에 어떻게 놓이는지 살펴봅니다. 앞 글 프로그램이 하드웨어를 움직이는 과정의 M[20] 같은 표기가 정확히 무엇을 가리키는지가 여기서 정해집니다.
비트, 바이트, 워드
비트(bit)는 0이나 1 하나를 담는 가장 작은 자리입니다. 비트 n개로 만들 수 있는 서로 다른 패턴은 개입니다. 3비트면 000부터 111까지 8개이고, 이 패턴을 부호 없는 정수로 읽으면 0부터 7까지입니다. 8비트를 묶은 것을 바이트(byte)라고 합니다.
워드(word)는 CPU가 한 번에 다루는 기본 폭을 가리키는 말입니다. 다만 그 크기가 시스템마다 달라서, 16비트를 워드라고 부르는 자료도 있고 32비트나 64비트를 워드라고 부르는 자료도 있습니다. 그래서 워드라는 말을 쓸 때는 "이 모델에서 워드는 32비트"처럼 크기를 함께 정해야 합니다.
용량의 단위도 기준을 확인해야 합니다. 국제단위계의 접두어를 따르는 kB, MB, GB는 1000의 거듭제곱이고, 이진 접두어를 쓰는 KiB, MiB, GiB는 1024의 거듭제곱입니다. 1 KiB = 1024바이트이므로 16 × 1024 = 16384바이트는 16 KiB입니다. Windows가 1 TB 디스크를 약 931 GB로 보여 주는 것은 용량이 줄어서가 아니라, 바이트를 으로 나누어 GiB 단위로 보여 주면서 이름만 GB로 붙였기 때문입니다.
주소는 바이트마다 붙는다
메모리의 각 위치를 가리키는 번호를 주소(address)라고 합니다. 이 교재는 바이트 하나마다 주소 하나를 붙이는 바이트 주소 방식(byte addressing)을 씁니다. 주소 0, 1, 2는 서로 다른 1바이트 위치입니다. 주소가 n비트이면 주소 패턴이 개이므로 바이트를 가리킬 수 있고, 이 범위를 주소 공간(address space)이라고 합니다.
주소가 8비트이면 주소 공간은 256바이트이고, 32비트이면 바이트, 곧 4 GiB입니다. 32비트 주소를 쓰는 시스템에서 메모리를 4 GiB보다 많이 바로 가리킬 수 없었던 이유가 이것입니다. 다만 주소 공간은 가리킬 수 있는 범위일 뿐이며, 그만큼의 메모리가 실제로 꽂혀 있다는 뜻은 아닙니다. 처음 질문으로 돌아가면, "32비트 컴퓨터"의 32비트가 주소의 폭이라면 메모리는 32바이트가 아니라 최대 4 GiB까지 가리킬 수 있다는 뜻입니다.
여러 바이트로 된 값과 정렬
4바이트 정수를 주소 8부터 저장하면 주소 8, 9, 10, 11의 네 자리를 차지하고, 다음 값은 주소 12부터 놓을 수 있습니다. 값의 시작 주소가 그 값의 크기의 배수가 되도록 놓는 것을 정렬(alignment)이라고 합니다. 4바이트 값을 4바이트 정렬하면 시작 주소가 4의 배수입니다.
정렬이 필요한 이유는 하드웨어가 메모리를 정해진 크기의 덩어리로 읽기 때문입니다. 4바이트씩 읽는 통로에서 주소 8의 4바이트 값은 한 번에 읽히지만, 주소 10부터 놓인 4바이트 값은 두 덩어리에 걸쳐 있어 두 번 읽어야 할 수 있습니다. 정렬되지 않은 접근을 허용하는지, 허용하면 얼마나 느린지는 CPU의 명령어 집합마다 다르므로, 이 교재의 모델에서는 4바이트 값의 시작 주소를 4의 배수로 제한합니다.
더 깊이: RISC-V는 정렬되지 않은 접근을 어떻게 정해 두었나
RISC-V의 기본 정수 명령어 규격을 보면 실제 ISA가 이 문제를 어떻게 정해 두는지 알 수 있습니다. 주소가 접근하는 크기의 배수인, 곧 자연스럽게 정렬된 읽기와 쓰기는 어떤 실행 환경에서도 주소 정렬 예외를 일으키지 않습니다. 정렬되지 않은 접근의 동작은 실행 환경 인터페이스(execution environment interface, EEI)가 정합니다. EEI는 이런 접근을 빠짐없이 지원하도록 정할 수 있고, 그러면 실행 환경이 하드웨어로, 또는 프로그램이 모르게 일어나는 트랩과 소프트웨어 처리로 이를 실행합니다. 반대로 EEI가 이를 보장하지 않으면 정렬되지 않은 접근은 성공할 수도 있고 예외를 일으킬 수도 있습니다.
규격은 정렬되지 않은 접근이 성공하더라도 구현에 따라 아주 느릴 수 있다고 경고합니다. 보이지 않는 트랩으로 처리되는 경우가 그렇습니다. 또 정렬된 읽기와 쓰기는 원자적으로 실행되지만 정렬되지 않은 것은 그렇지 않을 수 있어서, 원자성이 필요하면 따로 동기화해야 합니다. 한 번에 읽은 값이 다른 코어가 쓰기 전의 바이트와 쓴 뒤의 바이트가 섞인 것일 수도 있다는 뜻입니다.
같은 값의 바이트 순서
16진수 0x12345678을 바이트로 나누면 12, 34, 56, 78의 네 바이트입니다. 여기서 12가 가장 큰 자리를 맡는 최상위 바이트이고 78이 최하위 바이트입니다. 이 네 바이트를 주소 8부터 11까지 어떤 순서로 놓을지 정하는 방식을 엔디언(endianness)이라고 합니다.
| 주소 | 리틀 엔디언 | 빅 엔디언 |
|---|---|---|
| 8 | 78 | 12 |
| 9 | 56 | 34 |
| 10 | 34 | 56 |
| 11 | 12 | 78 |
리틀 엔디언(little-endian)은 최하위 바이트를 가장 낮은 주소에, 빅 엔디언(big-endian)은 최상위 바이트를 가장 낮은 주소에 놓습니다. 바이트의 순서가 바뀔 뿐 각 바이트 안의 비트 순서가 뒤집히는 것은 아니며, 같은 방식으로 읽기만 하면 값은 똑같습니다. x86 CPU와 대부분의 ARM 시스템은 리틀 엔디언을 쓰고, 인터넷 프로토콜은 빅 엔디언을 표준 순서로 씁니다. 그래서 서로 다른 순서를 쓰는 곳 사이에서 데이터를 주고받을 때는 바이트 순서를 변환해야 합니다.
value = 0x12345678
little = value.to_bytes(4, "little")
big = value.to_bytes(4, "big")
print(little.hex(" "), "|", big.hex(" "))
print(hex(int.from_bytes(little, "little")), hex(int.from_bytes(big, "big")))
print(hex(int.from_bytes(little[:2], "little")), hex(int.from_bytes(big[:2], "big")))
print(10**12 / 2**30)78 56 34 12 | 12 34 56 78
0x12345678 0x12345678
0x5678 0x1234
931.3225746154785to_bytes(4, "little")은 정수를 4바이트로 나누어 리틀 엔디언 순서로 늘어놓고, int.from_bytes는 거꾸로 바이트들을 정해진 순서로 읽어 정수를 만듭니다. hex(" ")는 바이트를 16진수로 적되 사이에 공백을 넣습니다. 첫 줄은 표와 같은 순서이고, 둘째 줄은 같은 순서로 읽으면 두 방식 모두 원래 값이 된다는 것을 보여 줍니다. 셋째 줄은 앞의 두 바이트, 곧 주소 8과 9만 읽은 결과인데, 리틀 엔디언에서는 하위 절반 0x5678이, 빅 엔디언에서는 상위 절반 0x1234가 나옵니다. 마지막 줄은 앞에서 말한 1 TB가 약 931 GiB라는 계산입니다.
정리
- 비트 n개는 가지 패턴을 만들고, 바이트는 8비트이며, 워드의 크기는 시스템마다 정해야 합니다.
- kB, MB, GB는 1000의 거듭제곱, KiB, MiB, GiB는 1024의 거듭제곱이므로 용량을 계산할 때 기준을 확인해야 합니다.
- 바이트 주소 방식에서 n비트 주소는 바이트의 주소 공간을 가리키며, 주소 공간이 실제 메모리의 크기는 아닙니다.
- k바이트 값은 연속된 k개의 주소를 차지하고, 정렬은 그 시작 주소를 값의 크기의 배수로 맞추는 것입니다.
- 엔디언은 여러 바이트 값의 바이트를 주소에 놓는 순서이며, 리틀 엔디언은 최하위 바이트를, 빅 엔디언은 최상위 바이트를 낮은 주소에 둡니다.
연습 문제
1. 주소와 용량
주소가 10비트인 바이트 주소 공간에 2바이트 정수 12개를 주소 100부터 빈틈없이 저장합니다. 주소 공간의 크기와, 마지막 정수의 시작 주소와 마지막 바이트의 주소를 구하세요. 2바이트 정렬을 요구하는 값을 주소 101부터 저장해도 되나요?
해설 보기
주소 공간은 바이트입니다. 첫 정수를 0번째로 세면 마지막 정수는 11번째이므로 에서 시작해 123까지 씁니다. 101은 홀수라 2의 배수가 아니므로 2바이트 정렬 조건을 만족하지 않습니다. 주소가 10비트라고 해서 값도 10비트인 것은 아니며, 주소의 폭과 데이터의 폭은 따로 정해집니다.
2. 디스크 용량 읽기
500 GB라고 적힌 저장장치를 운영체제가 GiB 단위로 보여 주면 약 몇 GiB인가요?
해설 보기
500 GB는 바이트이고, 1 GiB는 바이트이므로 , 곧 약 465.7 GiB입니다. 용량이 사라진 것이 아니라 같은 바이트 수를 다른 단위로 나눈 것입니다. 단위의 기준이 1000인지 1024인지를 적어 두지 않으면 7% 정도의 차이를 오류로 오해하기 쉽습니다.
3. 엔디언에 따라 읽은 값
메모리의 주소 20부터 23까지에 바이트 01 00 00 00이 들어 있습니다. 이것을 4바이트 정수로 읽으면 리틀 엔디언과 빅 엔디언에서 각각 얼마인가요?
해설 보기
리틀 엔디언은 낮은 주소의 바이트를 최하위 바이트로 읽으므로 값은 0x00000001, 곧 1입니다. 빅 엔디언은 낮은 주소의 바이트를 최상위 바이트로 읽으므로 0x01000000, 곧 입니다. 같은 바이트라도 읽는 순서를 잘못 정하면 1이 1600만이 될 수 있습니다. 파일이나 네트워크에서 받은 바이트를 정수로 바꿀 때 순서를 명시해야 하는 이유입니다.
주소를 계산할 때는 값이 시작하는 주소와 차지하는 마지막 주소를 따로 적어 보세요. 다음 글 진법과 고정 길이 정수에서는 이 바이트들에 담긴 비트 패턴을 수로 읽는 방법을 다룹니다.