빅엔디안과 리틀엔디안이란 무엇인가?
왜 바이트 저장 순서를 반대로 둘까?
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
코딩하다 보면 네트워크로 데이터를 보냈거나 특정 파일을 읽어왔는데, 값이 내 생각과 다르게 완전히 뒤집혀서 나오는 경우가 있다. 이 현상을 정식 명칭으로 Endianness(엔디안) 배열 방식, 즉 Big Endian(빅 엔디안)과 Little Endian(리틀 엔디안) 이라고 부른다.
CPU는 4 Byte 짜리 int형 데이터를 메모리에 저장할 때, 통째로 쑤셔 넣지 않고 1 Byte씩 쪼개서 주소에 차례대로 담는다. 이때 가장 큰 자릿수(MSB)부터 순서대로 넣을지, 가장 작은 자릿수(LSB)부터 역순으로 넣을지에 따라 데이터가 메모리에 배치되는 모양이 완전히 달라지게 된다.
사람이 숫자를 읽는 것처럼 직관적인 순서대로 넣으면 빅 엔디안, 기계가 연산하기 편하게 거꾸로 넣으면 리틀 엔디안이다. 우리가 주로 쓰는 인텔이나 AMD CPU 등 현대 PC는 대부분 리틀 엔디안 방식을 채택하고 있다.
코드로 직접 확인해 보자.
#include <iostream>
#include <iomanip>
int main() {
// 4 Byte 크기의 16진수 정수형 데이터
unsigned int number = 0x12345678;
// 메모리에 1 Byte씩 어떻게 저장되어 있는지 확인하기 위해 포인터 캐스팅
unsigned char* bytePtr = reinterpret_cast<unsigned char*>(&number);
for (int i = 0; i < sizeof(int); ++i) {
std::cout << "&bytePtr[" << i << "] : 0x"
<< std::hex << std::setw(2) << std::setfill('0')
<< (int)bytePtr[i] << std::endl;
}
/*
* 리틀 엔디안 시스템 기준
* &bytePtr[0] : 0x78 // 가장 작은 단위(LSB)가 가장 낮은 주소에 먼저 들어감
* &bytePtr[1] : 0x56
* &bytePtr[2] : 0x34
* &bytePtr[3] : 0x12
*
* 빅 엔디안 시스템 기준
* &bytePtr[0] : 0x12 // 가장 큰 단위(MSB)가 가장 낮은 주소에 먼저 들어감
* &bytePtr[1] : 0x34
* &bytePtr[2] : 0x56
* &bytePtr[3] : 0x78
*/
return 0;
}
여기서 궁금한 점이 생긴다. 어차피 똑같은 값을 저장하는 건데, 굳이 왜 리틀 엔디안처럼 데이터를 뒤집어서 저장하는 비직관적인 방식을 채택했을까? 이건 CPU 내부의 가산기(Adder) 설계와 형변환(Type Casting) 효율성 때문이다.
CPU가 두 숫자를 더하거나 뺄 때는 1의 자리, 즉 가장 작은 자릿수부터 계산해야 올림수(Carry) 처리가 가능하다. 리틀 엔디안은 가장 작은 단위(LSB)가 메모리의 가장 첫 번째 주소에 위치하므로, CPU가 메모리를 읽자마자 곧바로 산술 연산을 시작할 수 있다. 또한, 4 Byte 정수를 1 Byte(char)로 잘라내는 다운캐스팅을 할 때도 추가적인 주소 계산 없이 메모리의 시작 주소만 그대로 쏙 빼오면 되기 때문에 하드웨어 로직이 엄청나게 단순해진다.
반면에 빅 엔디안은 메모리 덤프를 눈으로 볼 때는 사람이 읽기 직관적이고, 두 숫자의 대소 비교를 할 때는 앞자리부터 비교하므로 유리하다는 장점이 있다. 그래서 데이터를 주고받을 때 혼선이 오지 않도록, 인터넷 프로토콜(TCP/IP) 같은 네트워크 표준 통신에서는 빅 엔디안 방식을 표준으로 사용한다.