Proven C Book←↑→

15 그래서 C는 추상적인 언어다

먼저 알아야 할 것

14장 컴파일러 최적화 · 컴파일러가 지키는 것은 관찰 가능한 결과뿐이다
2장 단순한 기계 모델 · CPU·메모리·클록의 세 부품 그림

돌아보기

14장에서 “C 프로그래머가 상대하는 것은 실제 기계가 아니다”라고 했다. 그러면 2–3장 내내 실제 기계(클록, 사물함, 워드)를 그렇게 들여다본 것은 헛수고였는가?

답. 반대다. 그 그림들이 바로 추상 기계의 뼈대다. 표준의 추상 기계는 허공에서 지어낸 것이 아니라 실제 기계들의 공통 뼈대(바이트 열의 메모리, 주소, 순차 실행)를 추려 계약서에 옮겨 적은 것이다. 실제 기계를 알아야 추상 기계가 왜 그 모양인지 이해되고, 추상 기계를 알아야 실제 기계의 복잡함(12–14장)에 흔들리지 않는다. 두 그림은 경쟁자가 아니라 한 벌이다.

이 장의 필요성과 맥락

2부를 닫는 자리이므로 새 지식이 아니라 합침이다. 흩어 놓은 세 조각(사다리가 된 기억, 겹쳐 도는 실행, 고쳐 쓰는 컴파일러)을 여기서 하나의 명제로 묶지 않으면, 독자는 2부를 「잡학 열 장」으로 기억하게 된다. 그리고 이 명제, 곧 C는 추상 기계의 언어라는 명제를 3부의 첫 프로그램 앞에 세워 두어야, 그 프로그램을 기계어를 옮겨 적은 것으로 읽는 습관이 애초에 생기지 않는다.

이 장이 끝나면

제2부의 결론이다. 흩어져 있던 조각(사다리가 된 기억, 겹쳐 도는 실행, 편집자 노릇을 하는 컴파일러)을 모아 하나의 명제로 완성한다: C는 기계어를 옮겨 적은 것이 아니라 추상 기계의 언어이고, 그래서 추상적으로 접근해야 한다. 요즘 더 엄격해지는 포인터 규칙(프로버넌스)이 왜 그 귀결인지 보고, 마지막으로 CPU와 GPU를 대조하며 C가 “어느 기계의” 언어인지로 부를 닫는다.

이 장에서 답할 질문

  1. 4장의 태그 포인터(주소의 빈 비트에 딱지를 끼우는 재주)도 이 규칙과 부딪히는가?

15.1 논제의 완성 — 기억·실행·컴파일러, 세 조각을 합치다#

이 부가 쌓아 온 조각을 한 줄씩으로 줄이면 이렇다.

그러면 소스 코드의 한 줄 한 줄이 “기계가 그대로 할 일”이라는 생각은 세 겹으로 무너진다. 남는 단단한 것은 하나 — 계약이다. 표준이라는 계약서가 정의한 가상의 기계(추상 기계) 위에서 내 코드가 무엇을 뜻하는가. 그것만이 C 프로그램의 진짜 의미이고, 나머지 전부(캐시 적중, 파이프라인, 재배열, 레지스터 배치)는 그 의미를 빠르게 실현하는 속사정이다.

그래서 C는 추상적인 언어이고, 추상적으로 접근해야 한다. “이 코드를 기계가 어떻게 돌릴까”를 짐작하는 습관이 아니라, “이 코드가 계약 위에서 무엇을 뜻하는가”를 묻는 습관 — 이 책이 이후 모든 장에서 기르려는 것이 바로 그 습관이다.

흔한 오해. “C는 하드웨어를 직접 조작하는 언어다”

절반의 역사와 절반의 오해가 섞인, C에 관한 가장 유명한 문장이다. 역사적으로는 사실에 가까웠고(2–3장), 지금도 C는 하드웨어에 가장 가까운 곳까지 데려다주는 언어다. 그러나 오늘의 C 코드와 하드웨어 사이에는 편집자(컴파일러)와 계약서(표준)가 서 있고, 순진한 “직접 조작” 감각으로 쓴 코드(이를테면 계약 밖의 지름길)는 편집자의 재량 앞에서 부서진다(14장의 사라진 지우개, 그리고 56장). 정확한 문장은 이렇다: C는 하드웨어를 직접 조작하는 언어가 아니라, 하드웨어를 추상 기계라는 계약으로 만나게 해 주는 언어다.

15.2 요즘의 흐름 — 포인터에 붙는 출처#

C를 추상 기계의 언어로 보는 이 관점이 요즘 어디로 가고 있는지 보여 주는 대표적인 흐름이 포인터 규칙의 정밀화다.

3장에서 “주소도 수”라고 했다. 순진한 그림에서 포인터는 그냥 번호다 — 같은 번호면 같은 곳이고, 번호만 맞으면 어떻게 얻었든 그곳을 만질 수 있어야 한다. 그러나 편집자의 세계에서 이 그림은 유지될 수 없다. 컴파일러의 최적화는 “이 포인터는 어디에서 왔는가”라는 계보 추적 위에 서 있기 때문이다 — 이 변수에서 나온 포인터는 저 변수를 건드릴 수 없다는 전제(14장의 엄격한 앨리어싱도 그 한 갈래)가 있어야 재배열과 캐싱을 할 수 있다.

그래서 현대의 표준 논의는 포인터를 “번호”가 아니라 “번호 + 출처 딱지”로 다듬어 가고 있다. 같은 비트 패턴이라도 어디에서 유래했는가에 따라 쓸 수 있는 곳이 다르다. 이 출처 개념을 프로버넌스(provenance)라 부르고, C 표준 위원회는 이를 명문화하는 작업을 진행해 왔다. 방향은 일관된다: 포인터에 대한 규칙이 점점 더 명시적이고 엄격해지는 쪽, 즉 계약서의 조항이 더 정밀해지는 쪽이다. 입문 단계에서 조항의 세부까지 알 필요는 없다 — “포인터는 그냥 수가 아니다, 계보가 있다”는 감각만 여기서 심고, 실무 규칙은 38장에서, 계약 위반의 결과는 56장에서 만난다.

문. 4장의 태그 포인터(주소의 빈 비트에 딱지를 끼우는 재주)도 이 규칙과 부딪히는가?

답. 부딪힐 수 있는 지점이 정확히 그곳이다. 포인터를 수처럼 주물러 태그를 끼우고 떼는 일은 “번호 = 포인터” 그림에서는 자명했지만, 출처 딱지의 세계에서는 계보를 잃지 않는 절차를 지켜야 하는 일이 된다(정수로 변환해 다루는 합법적 경로가 마련되어 있다). 저수준 기법일수록 계약서를 정독해야 하는 시대 — 그것이 모던 C다.

15.3 마무리 파노라마 — C는 어느 기계의 언어인가#

부를 닫기 전에, 시야를 한 번 넓힌다. “기계”라고 불러 온 것이 사실 한 종류가 아니기 때문이다.

같은 트랜지스터로 정반대의 기계를 지을 수 있다. 하나는 한 가지 일을 최대한 빨리 끝내는 기계다. 깊은 파이프라인, 큰 캐시, 정교한 분기 예측(12–13장)은 모두 지연시간(latency)을 줄이는 장치다. 우리가 여태 그려 온 CPU다. 다른 하나는 같은 일을 산더미에 한꺼번에 하는 기계 — 예측도 깊은 파이프라인도 덜어 내고, 그 자리에 단순한 계산기를 수천 개 심는다. 처리량(throughput)의 기계, GPU다. 화면의 수백만 픽셀에 같은 계산을 뿌리려고 태어났고, 그 체질이 행렬 계산(인공지능의 심장)과 맞아떨어지며 시대의 주인공이 됐다.

식당으로 비유하면, CPU는 어떤 주문이든 즉시 쳐내는 달인 셰프 한두 명이고, GPU는 같은 메뉴를 만 그릇씩 동시에 만드는 급식 라인이다. 어느 쪽이 낫냐는 물음은 성립하지 않는다 — 주문이 다르다.

C와의 관계가 이 장의 마지막 문장이 된다. GPU의 세계조차 그 프로그래밍 언어(CUDA, OpenCL)는 C의 방언으로 만들어졌다 — 시스템 세계의 공용어가 C라는 1장의 이야기가 여기서도 반복된다. 그러나 정확히 말하면, C의 추상 기계는 CPU의 상(像)이다. 순차 실행, 하나의 메모리, 주소 — 계약서의 가상 기계는 CPU의 뼈대를 본떴고, GPU라는 다른 체질은 그 계약서의 방언과 확장으로 다룬다. C를 배운다는 것은 컴퓨팅의 여러 기계 가운데 가장 근본이 되는 상(像), 곧 폰 노이만 이래의 순차 기계를 그 언어와 함께 배우는 일이다.

15.4 제2부를 닫으며#

바닥 공사가 끝났다. 제1부에서 기계를 세 부품으로 그리고(2장) 사물함 복도를 걸으며 그 복도가 어떻게 나뉘는지 보았고(3–5장), 이 부에서 정수와 소수와 글자와 흐름을 담는 법을 배웠고(6–9장), 실행된다는 것이 무슨 일이며 그 밑의 기계가 왜 복잡해졌는지, 그 위에 선 계약이 무엇인지 보았다(10–15장). 이 모든 것이 심화 문답으로 계속 소환될 밑천이다.

이제 정말로 시작할 수 있다. 다음 부에서 드디어 첫 프로그램을 쓴다 — 지면 위에 헬로 월드가 찍히고, 그 한 편을 읽어 내는 여정이 책의 나머지 전부다.