Hacker News

C's Flexible Integer Sizes Were Not a Design Mistake

C의 유연한 정수 크기는 설계 실수가 아니었습니다

C의 int와 long이 고정된 비트 수를 보장하지 않는 이유를 1970년대의 다양한 컴퓨터 구조와 C의 이식성 목표에서 설명합니다. 글은 최소 범위를 보장하면서 하드웨어에 맞춰 타입 크기를 정한 선택이 당시에는 효율적인 코드 생성과 여러 기종 지원에 도움이 됐다고 주장하고, 오늘날의 고정 폭 정수 타입과 남은 불편도 함께 다룹니다.

AI 요약

C를 처음 배우는 개발자는 int가 늘 32비트라고 가정하기 쉽지만, C의 기본 정수형은 기계마다 크기가 다릅니다. 글은 이 설계가 단순한 실수가 아니라, 1970년대의 여러 컴퓨터에서 효율적으로 동작하면서도 이식성을 확보하려는 선택이었다고 설명합니다. 당시에는 12비트부터 60비트까지 다양한 워드 크기가 쓰였고, 문자 크기도 6·7·8·9비트로 제각각이었습니다. 음수 표현 방식과 메모리 주소 지정 방식도 오늘날처럼 통일되지 않았습니다.

int는 기계의 자연스러운 정수형이었습니다

C의 조상인 BCPL과 B는 값을 기계 워드 하나로 다뤘습니다. PDP-11로 옮겨가자 바이트 주소 지정과 부동소수점 처리에 맞지 않는 부분이 드러났고, C는 char로 문자를 다루면서 int를 기계가 효율적으로 처리하는 자연스러운 정수형으로 두었습니다. 따라서 int가 반드시 32비트라는 약속은 처음부터 없었습니다.

글은 1978년 《The C Programming Language》에 실린 표를 들어 당시의 차이를 보여줍니다. PDP-11에서는 int가 16비트였고, Honeywell 6000에서는 36비트, IBM 370과 Interdata 8/32에서는 32비트였습니다. char도 기계에 따라 8비트 또는 9비트였습니다. 같은 C와 상당수의 같은 프로그램을 각 기계의 성능을 살려 실행할 수 있었던 배경으로 이 유연성을 꼽습니다.

고정 크기를 강제하면 생기는 비용

16비트 기계에서 int를 32비트로 고정하면 덧셈에 하위 워드의 ADD와 상위 워드의 ADC가 필요하고, 비교·시프트·곱셈도 더 많은 명령을 씁니다. 배열 인덱스와 반복문의 카운터까지 더 큰 값을 사용하므로 레지스터와 명령 비용도 늘어납니다. 반대로 36비트 기계에서는 정확히 32비트로 계산하려면 연산 뒤 마스킹과 부호 확장을 해야 할 수 있습니다.

음수 표현이 2의 보수로 통일되지 않았던 점도 고려해야 했습니다. 1의 보수나 부호-크기 표현을 쓰는 기계에서 2의 보수 기준의 오버플로 동작을 강제하면 추가 처리가 필요합니다. C는 여러 음수 표현을 허용하고 부호 있는 정수의 오버플로를 정의하지 않는 방식으로 기계별 차이를 남겨뒀습니다. 바이트 주소 지정이 없는 기계나 9비트 문자를 쓰는 기계도 고려해 char가 정확히 8비트라고 정하지 않았습니다. 다만 int에는 최소 범위를 둬서 적어도 -32767부터 32767까지 표현하도록 했습니다.

정확한 크기보다 보장 범위

ANSI C 표준은 각 정수형에 정확한 크기 대신 최소 범위를 보장하는 접근을 택했습니다. C89의 <limits.h> 값에 따라 필요한 범위를 담을 수 있는 타입을 고르는 방식입니다. 예를 들어 100,000까지 저장해야 한다면 최소 32비트 범위를 보장하는 long을 쓸 수 있습니다. C99의 <stdint.h>는 int32_t 같은 정확한 폭의 타입과 int_least32_t, int_fast32_t 같은 타입을 추가했습니다. 정확한 폭의 타입은 해당 구현이 그 폭을 지원할 때만 제공됩니다.

글은 Lua 5.4를 현대적인 사례로 듭니다. Lua는 int가 32비트 이상인지 UINT_MAX로 확인하고, 조건에 따라 unsigned int나 unsigned long을 VM 명령어 타입으로 선택합니다. 이 타입은 이름에 32가 들어가도 정확히 32비트일 필요는 없고, 적어도 32비트 범위를 담으면 됩니다. 문자열의 바이너리 입출력에서도 CHAR_BIT를 기준으로 바이트 단위 값을 다루며, 엔디언을 직접 처리합니다. 글은 이런 방식이 고정된 8비트 바이트를 전제하지 않는 C 구현까지 고려하는 코드의 사례라고 설명합니다.

지금도 남은 비용과 적용 범위

유연한 타입 크기는 코드가 int를 16비트나 32비트로 가정할 때 이식성 문제를 낳았습니다. 포인터를 int에 저장하거나 sizeof(int)와 sizeof(void *)가 같다고 가정한 코드도 문제가 됩니다. 64비트 환경에서도 유닉스 계열의 LP64와 64비트 Windows의 LLP64가 달라 long의 크기를 혼동하기 쉽습니다. 고정 폭 타입이 C99에 들어오기 전에는 프로젝트마다 typedef와 조건부 컴파일로 타입을 정의해야 했습니다.

글은 이런 불편을 인정하면서도, 모든 프로젝트가 이식성을 요구하지는 않는다고 덧붙입니다. 특정 PlayStation 하드웨어와 해당 SDK만 대상으로 한다면 타입 크기를 미리 알고 기본 타입을 써도 됩니다. 반대로 바이너리 형식이나 여러 플랫폼에서 공유하는 데이터처럼 폭을 정확히 맞춰야 한다면 <stdint.h>를 사용해야 합니다. C23이 부호 있는 정수의 2의 보수 표현을 요구한 점도, 표준이 하드웨어의 현실이 바뀐 뒤 규칙을 조정한 사례로 소개합니다.

Hacker News 반응

  • @usrnm — 고정 크기 정수가 없었던 점은 실수였다고 봅니다. 포인터 크기에 맞춘 정수도 쓸모가 있습니다.
    • @quelsolaar — 당시에는 고정 크기를 어떻게 정해야 하는지 알기 어려웠을 겁니다.
    • @tialaramex — 포인터와 같은 크기의 정수가 꼭 필요하지는 않습니다. CHERI처럼 포인터에 capability 비트를 넣을 수 있는데, 정수에도 그 비트가 들어가길 바라지는 않을 겁니다. Rust도 처음에는 usize와 isize가 포인터 크기라고 했지만, 나중에는 주소 크기라고 정정했습니다.
  • @quelsolaar — 좋은 글입니다. C가 이런 유연성을 갖추지 않았다면 살아남지 못했을 겁니다. 역사에만 해당하는 이야기도 아닙니다. 오늘날 DSP 같은 플랫폼에는 주소를 지정할 수 있는 최소 단위가 32비트인 경우도 있습니다. 이런 플랫폼도 C를 도구 체인에 쓰며, 새 언어나 방언을 만들지 않고 프로그래밍할 수 있다는 점은 큰 이점입니다.
    • @pjmlp — C가 살아남은 건 UNIX가 이끌었기 때문입니다.
    • @flohofwoe — 동의하지 않습니다. Linux가 데이터센터에서 자리를 잡기 전부터 C와 C++는 UNIX 밖에서도 널리 쓰였습니다. C가 새 하드웨어에 쉽게 맞춰졌기 때문에 성공한 겁니다.
  • @adrian_b — 유연한 크기는 C가 만들어질 당시에는 필요했지만, 1990년에는 이미 낡은 선택이었다고 봅니다. 여러 종류의 컴퓨터에서 C를 썼지만, 고정 크기 정수만 사용한 프로그램에서만 이식성 문제가 없었습니다.
    • @locknitpicker — 한 가지 아키텍처만 다룬 경험과 여러 아키텍처가 필요 없다는 주장은 다릅니다. 오늘날에도 16비트 이하 프로세서가 팔리고, DSP에는 32비트보다 큰 int를 쓰는 경우도 있습니다.
  • @lexicality — int_fast32_t와 int_least32_t가 “int는 임의 크기이니 알아서 하라”는 방식보다 낫습니다. 그런 타입만 쓰면 값의 의도를 컴파일러와 다음 코드 작성자에게 정확히 전달할 수 있습니다.
    • @mmoll — 저도 같은 말을 하려고 왔습니다. 저장에는 int_leastN_t를, 계산에는 int_fastN_t를 쓰면 됩니다. stdint.h는 필요 이상으로 나쁜 평가를 받습니다.
  • @RobotToaster — 현대 시스템 대부분에서 자연스러운 크기를 뜻한다면 int는 64비트여야 하지 않나요?
    • @sparkie — x86-64에서 32비트 연산은 인코딩이 더 간결하고, 컴파일러도 상위 32비트가 필요 없으면 32비트 명령을 냅니다. 64비트로 바꾸면 기존 코드와 데이터 형식을 깨뜨릴 수 있어 int를 32비트로 둔 선택은 타당했습니다.
  • @nayuki — 글의 Lua 코드 예시에는 이식성 문제가 있을 수 있습니다. int가 16비트일 때 UINT_MAX를 30비트만큼 시프트하면 타입 폭보다 큰 시프트가 되어 정의되지 않은 동작이 아닌가요? CHAR_BIT가 16이고 int도 16비트라면 1 << NB도 같은 문제가 있습니다. C의 정수 크기가 유연하면 이식 가능한 코드를 쓰는 데 더 많은 노력이 든다는 점도 분명합니다.
  • @layer8 — C에서 char의 크기는 메모리에서 정확히 1바이트입니다. 다만 C의 바이트가 반드시 8비트인 것은 아닙니다. 바이트는 포인터로 주소를 지정하는 최소 메모리 단위입니다.
    • @kvemkon — TI C55x DSP는 16비트 바이트를 씁니다. 지금도 새 제품을 살 수 있습니다.
  • @codedokode — int 크기가 다르면 프로그래밍이 어려워집니다. 레코드가 100,000개라면 int로 번호를 저장해도 되는지, 기계가 16비트 int를 쓰면 어떻게 되는지 판단해야 합니다. 기계 간 데이터 전송에도 int를 그대로 쓰기 어렵습니다.
  • @habitue — 의도된 설계였다는 점은 맞지만, 결과적으로 실수였다고 봅니다. 64비트 환경으로 옮길 때 int를 8바이트로 만들지 않았다는 사실은 설계가 더는 맞지 않는다는 점을 보여줍니다.
    • @sparkie — amd64에서 int를 32비트로 둔 건 옳았습니다. 32비트 코드를 거의 그대로 실행할 수 있었고, 64비트 연산은 명시적으로 선택할 수 있었습니다. 기존 소프트웨어를 깨뜨리지 않는 일이 중요했습니다.

원문: Pikuma / 번역·요약: Trawling