Proven C BookEnglish GitHub

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

먼저 알아야 할 것

13장 컴파일러 최적화 · 컴파일러가 지키는 것은 관찰 가능한 동작뿐이다
4장 단순한 기계 모델 · 단순한 기계 모델

돌아보기

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

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

이 장의 필요성과 맥락

2부를 닫는 자리이므로 새 지식이 아니라 합침이다. 흩어 놓은 세 조각을 여기서 하나의 명제로 묶지 않으면, 독자는 2부를 「잡학 열한 장」으로 기억하게 된다. 그리고 이 명제 — C 는 추상 기계의 언어다 — 를 3부의 첫 프로그램 앞에 세워 두어야, 그 프로그램을 기계어의 별명으로 읽는 습관이 애초에 생기지 않는다.

이 장이 끝나면

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

이 장에서 답할 질문

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

14.1 논제의 완성 — 세 조각을 합치다

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

14.4 제2부를 닫으며

바닥 공사가 끝났다. 비트에서 시작해(4장) 사물함 복도를 걷고(5·6장), 정수와 소수와 글자와 흐름을 담는 법을 배웠고(7–10장), 기계가 복잡해진 사연과 그 위에 선 계약을 보았다(11–14장). 이 모든 것이 심화 문답으로 계속 소환될 밑천이다.

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