왜 ASCII에서 소문자는 대문자 바로 뒤에 오지 않을까?
(tylerhillery.com)- ASCII에서
Z는90,a는97에 배치되어 있으며, 그 사이 6개 문자 덕분에 대문자와 소문자의 코드 차이가 32로 맞춰짐 - 32는
2^5라서A65와a97처럼 대응하는 대소문자는 항상00100000비트 하나만 다름 - 이 배치 덕분에
32의 비트 반전값과 AND하면 대문자화,32와 OR하면 소문자화,32와 XOR하면 대소문자 반전이 가능함 - 알파벳 순번은 문자 코드에
31을 AND해 하위 5비트만 남기면 얻을 수 있으며,A/a는 1,Z/z는 26이 됨 - ASCII는 7비트로 128개 코드 포인트만 표현하는 초기 문자 인코딩이며, 오늘날 쓰이는 Unicode의 첫 128개 코드 포인트는 ASCII와 동일함
ASCII 배치와 32 차이
- ASCII 표에서 대문자
Z의 코드값은90이고, 소문자a는 바로 다음 값이 아니라97에 배치됨 - 그 사이에는
[\]^_`의 6개 문자가 있음 - 영어 알파벳은 26자이고, 이 6개 문자를 더하면
26 + 6 = 32가 됨 32는2^5에 해당하는 값이라, 대문자와 소문자 대응 관계가 특정 비트 하나의 차이로 정렬됨- 예를 들어
A는65이자01000001,a는97이자01100001이며, 두 값의 차이는32임
ASCII와 Unicode의 관계
- ASCII는 초기 문자 인코딩 방식 중 하나이며, 7비트만 사용해
2^7 = 128개의 코드 포인트를 표현함 - 128개 코드 포인트는 사람이 사용하는 모든 문자를 담기에는 부족하며, 특히 수만 개 문자를 가진 중국어 같은 언어까지 담기에는 충분하지 않음
- 오늘날 표준 문자 집합으로는 Unicode가 쓰이며, UTF-8과 UTF-16 같은 여러 인코딩을 가짐
- Unicode의 첫 128개 코드 포인트는 ASCII와 동일함
대소문자를 가르는 5번째 비트
- 대문자와 대응하는 소문자를 이진수로 비교하면 항상
32에 해당하는 비트가 바뀜
65 = 01000001 = A
97 = 01100001 = a
66 = 01000010 = B
98 = 01100010 = b
67 = 01000011 = C
99 = 01100011 = c
32는 이진수에서00100000이며, 이 비트 하나가 대문자와 소문자 차이를 만듦- ASCII의 알파벳 배치는 비트 연산으로 대소문자 변환을 쉽게 할 수 있게 되어 있음
비트 연산으로 대소문자 처리하기
-
대문자로 변환
- 문자를 대문자로 만들려면
32의 비트 반전값과 비트 AND를 수행함
- 문자를 대문자로 만들려면
0 1 1 0 0 0 0 1 (97 = 'a')
& 1 1 0 1 1 1 1 1 (mask)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')
a에 적용하면97이65로 바뀌어A가 됨- 이미 대문자인
A에 같은 연산을 적용하면 그대로A로 남음 -
소문자로 변환
- 문자를 소문자로 만들려면
32와 비트 OR를 수행함
- 문자를 소문자로 만들려면
0 1 0 0 0 0 0 1 (65 = 'A')
| 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
A에 적용하면65가97로 바뀌어a가 됨- 이미 소문자인
a에 같은 연산을 적용하면 그대로a로 남음 -
대소문자 반전
- 대소문자를 뒤집으려면
32와 비트 XOR를 수행함
- 대소문자를 뒤집으려면
0 1 1 0 0 0 0 1 (97 = 'a')
^ 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')
0 1 0 0 0 0 0 1 (65 = 'A')
^ 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
a는A로,A는a로 바뀜
하위 5비트로 알파벳 순번 얻기
- 알파벳 순번은 문자 코드에
31을 비트 AND해서 구할 수 있음
0 1 0 0 0 0 0 1 (65 = 'A')
& 0 0 0 1 1 1 1 1 (31)
-------------------
0 0 0 0 0 0 0 1 (1)
0 1 1 1 1 0 1 0 (122 = 'z')
& 0 0 0 1 1 1 1 1 (31)
-------------------
0 0 0 1 1 0 1 0 (26)
31은 이진수로00011111이라, 앞쪽 비트를 지우고 하위 5비트만 남김- ASCII에서 알파벳 문자의 하위 5비트는 알파벳 위치와 맞아떨어짐
A/a는00001로 끝나 1이 되고,B/b는00010으로 끝나 2가 되며,Z/z는11010으로 끝나 26이 됨- ASCII 문자 코드에서
c & 31은c % 32와 같음 32가 2의 거듭제곱이기 때문에,31로 마스킹하면32단위 묶음을 제거하고 남은 부분만 보존함
'A' = 65 → 65 % 32 = 1
'B' = 66 → 66 % 32 = 2
...
'Z' = 90 → 90 % 32 = 26
'a' = 97 → 97 % 32 = 1
'b' = 98 → 98 % 32 = 2
...
'z' = 122 → 122 % 32 = 26
Lobste.rs 의견들
-
설명은 괜찮지만 https://garbagecollected.org/2017/01/31/four-column-ascii/ 쪽이 더 잘 설명한다고 느낌
단순히 Shift만의 문제가 아니라 Ctrl도 관련됨. 예를 들어 Tab은 Ctrl-I인데, I가1001001이고 Ctrl이 첫 비트를 마스킹해서 Tab의0001001을 남기기 때문임 -
1960년대 제조사들처럼 전자식 논리가 아니라 전기기계식 논리를 쓴다면, bit paired keyboard가 훨씬 구현하기 쉬움
Shift 키는 ASCII 문자에서 비트 하나만 토글하면 됨. 지금은 모든 키보드에 범용 CPU를 넣고 논리가 사실상 공짜지만, 범용 컴퓨터가 방 하나 크기였던 시대에는 그런 해법이 훨씬 비쌌음 -
글은 ASCII 배치의 동기로 대소문자 변환을 비트 연산으로 효율적으로 구현할 수 있다는 점을 제시하지만, 그 가치는 역사적으로는 몰라도 현대적으로는 꽤 얇아 보임
a→A변환은 단순 텍스트에서만 통하고, ISO-8859-1이나 Unicode가ü,ç같은 문자에도 이 배치를 일부 확장하더라도 대소문자 변환은 지역·문맥·시점에 따라 달라짐.ß나ï의 올바른 대문자/소문자 대응은 언어, 규제 기관, 사용 지역, 시대에 좌우됨
Unicode는 https://www.unicode.org/charts/case/ 같은 대소문자 매핑과 접기 표를 제공해 흔한 경우에는 꽤 가까운 답을 주지만, 인간의 정책이 개입되는 문제라 복잡도는 사실상 무한하고 맞추려면 맞춤 소프트웨어가 필요할 수 있음
오늘날 Unicode의 처음 2⁷ 코드포인트에만 제한된다고 보장할 수 없다면0b0010_0000오프셋은 쉽게 깨지고, 단일 비트 연산보다 비싼 코드 경로가 필연적으로 들어감. CPython에서도''.upper는unicode_upper{,_impl}의 ASCII 빠른 경로를 거치지만, 실제로는 분기, 인라인 함수, 구조체 접근, 여러 함수와 함수, 매크로를 거쳐 테이블 조회를 수행함
현대 언어의''.upper구현은 대체로 비슷한 수준의 복잡도를 가질 것이고, 컴파일러가 최적화하더라도 현대적 접근은 단일 비트 연산보다 비싸질 수밖에 없어 보임. 이 설계의 이점은 원시 바이너리나 2⁷ 이하 코드포인트 Unicode 데이터를 담은dtype=uint8의numpy.ndarray에 산술 연산을 하는 식의 맞춤 소프트웨어에서나 두드러질 가능성이 큼
궁금한 건 이것임: 이 설계 선택의 유일한 동기가 글에서 말한 것이라고 가정하면, 당시에도 128바이트 조회 테이블이라는 대안이 있었을 텐데, 컴퓨팅 역사에서 단일 비트 연산이 128바이트 테이블 조회보다 더 효율적이거나 더 실현 가능했던 기간은 언제였을까?- 오래 성공한 대중 기술을 파고들면 이런 종류의 비트 트릭이 자주 보임
어느 시점의 설계 선택이, 나중에 깨질 가정에 기대어 무언가의 형태를 고정하고 이후 확장을 어렵거나 불가능하게 만듦. 최근 IPv6 관련 글들에서도 IPv4와 IPv6의 당시에는 옳았던 선택들이 오늘날에는 거대한 부담이 됐다는 점이 드러남
호의적으로 보면, 이런 선택은 위험과 이득을 저울질해 내려졌을 것임. “문자 하나를 대문자로 바꾸는 데 단일 비트 연산이면 훨씬 빠르니, 언젠가 일본어 사용자가 생겼을 때 깨질 위험을 감수할 만하다” 같은 식임
현대인의 입장에서는 1976년의 선택이 2026년의 삶을 더 어렵게 만들지만, 1976년에 컴퓨터를 가진 적이 없다면 그 이득은 누리지 못했고 단점만 남음. 이런 이기심을 내려놓고 보면, 20년 뒤에도 인기 있을 소프트웨어를 만든다고 할 때 이런 선택의 지속 가능성을 어떻게 더 잘 예측할지 궁금함 - ASCII 전용 대소문자 무시 인코딩을 쓰는 네트워크 프로토콜이 많아서, 빠른 비트 단위
tolower는 아직도 유용함. https://dotat.at/@/2022-06-27-tolower-swar.html
- 오래 성공한 대중 기술을 파고들면 이런 종류의 비트 트릭이 자주 보임
-
적어도 ASCII에서는 글자들이 연속되어 있음. EBCDIC은 간격이
0x40(64)이고, ASCII와 비교하면 9개짜리 줄 두 개와 8개짜리 줄 하나가 위아래로 쌓인 형태임
https://en.wikipedia.org/wiki/EBCDIC- EBCDIC 이전의 6비트 문자 코드 중에도 꽤 이상한 것들이 있었음. https://en.wikipedia.org/wiki/Six-bit_character_code/…
5비트 코드 http://www.quadibloc.com/crypto/images/tele38.gif 와, 기억이 맞다면 페이지 전환 제어 문자가 잔뜩 들어간 6비트 코드를 쓰던 일부 CDC 주변장치에 대해서는 말을 아끼는 편이 나음
- EBCDIC 이전의 6비트 문자 코드 중에도 꽤 이상한 것들이 있었음. https://en.wikipedia.org/wiki/Six-bit_character_code/…
-
IRC 닉네임이 ASCII 대소문자 무시라서
foo{가foo[와 같고bar|가bar\와 같았던 게 떠오름
아직도 일부 클라이언트가 이 때문에 헷갈려도 놀랍지 않음