TECH 으로 돌아가기
TECH HACKER NEWS 오늘 6분 읽기 33 READS

C 언어에도 '보장된' 꼬리 호출이 생겼어요 — musttail 이야기

함수형 언어 얘기할 때 단골로 나오는 '꼬리 호출 최적화(Tail-Call Optimization, TCO)'라는 게 있는데요. 놀랍게도 C 언어에서는 이걸 '확실하게 보장받는' 방법이 생긴 지 몇 년 안 됐다는 사실, 알고 계셨나요? LWN에 이 역사를 짚어주는 글이 올라왔길래, 오늘은 이 이야기를 풀어볼게요.

꼬리 호출이 뭐냐면

함수가 맨 마지막에 하는 일이 '다른 함수를 호출하고 그 결과를 그대로 반환'하는 것일 때, 이걸 꼬리 호출이라고 해요. 코드로 보면 return f(x); 같은 형태죠. 이 상황에서는 호출하는 쪽이 f를 부른 뒤에 할 일이 아무것도 없거든요. 그러니까 새 스택 프레임을 쌓는 대신, 지금 쓰던 프레임을 f에게 물려주고 그냥 '점프'해버려도 결과는 똑같아요. 이렇게 호출(call)을 점프(jump)로 바꿔주는 최적화가 TCO예요.

이게 왜 중요하냐면, 재귀를 아무리 깊게 돌려도 스택이 안 자라거든요. 재귀 깊이가 100만이어도 스택 오버플로가 안 나요. 함수 호출 오버헤드도 사라지니 성능에도 좋고요. Scheme 같은 언어는 아예 언어 표준에서 이걸 보장해서, 반복문 대신 재귀를 쓰는 게 자연스러운 문화죠.

C에서는 '복불복'이었어요

그런데 C 표준에는 TCO에 대한 언급이 한 줄도 없어요. GCC나 Clang이 -O2 최적화를 켜면 알아서 해주긴 하는데(sibling call optimization이라고 불러요), 이건 어디까지나 '해줄 수도 있는' 최적화거든요. 디버그 빌드에서는 안 해주고, 지역 변수의 주소를 인자로 넘기면 안 해주고, 호출 규약(calling convention, 함수끼리 인자를 주고받는 약속)이 안 맞아도 안 해줘요. 문제는 깊은 재귀의 '정확성'이 이 최적화에 달려 있는 코드라면, 최적화가 안 되는 순간 프로그램이 그냥 죽는다는 거예요. '되면 좋고 안 되면 말고'가 아니라 '안 되면 버그'인 상황인 거죠.

musttail의 등장

그래서 나온 게 [[clang::musttail]] 속성이에요. return 문 앞에 붙이면 컴파일러가 반드시 꼬리 호출로 컴파일하고, 어떤 이유로든 못 하면 아예 컴파일 에러를 내줘요. '조용히 실패'가 아니라 '시끄럽게 실패'하니까 비로소 믿고 쓸 수 있게 된 거죠. Clang은 2021년에 나온 버전 13부터 지원했고, GCC는 2025년에 나온 GCC 15에서야 지원하기 시작했어요. C가 반세기를 훌쩍 넘긴 언어라는 걸 생각하면 정말 최근 일인 거예요. LWN 기사가 '비교적 최근'이라고 짚은 게 바로 이 지점이에요.

어디에 쓰냐면: 인터프리터의 비밀 무기

이 기능이 빛나는 대표적인 곳이 인터프리터의 디스패치 루프예요. 전통적으로는 거대한 switch 문이나 computed goto로 바이트코드를 하나씩 처리하는데, 함수 하나가 너무 커지면 컴파일러가 레지스터 할당을 잘 못 해서 성능이 떨어지거든요. 대신 명령어(opcode)마다 작은 함수를 만들고, 각 함수 끝에서 musttail로 다음 명령어 처리 함수로 점프하게 만들면, 컴파일러가 함수 단위로 핵심 상태를 레지스터에 딱 유지해줘요. 구글의 protobuf 파서인 upb가 이 기법으로 파싱 속도를 크게 끌어올린 걸로 유명하고, CPython도 3.14에서 이 방식의 'tail-call 인터프리터'를 실험 옵션으로 넣었어요.

쓰기 전에 알아둘 것

musttail에는 조건이 있어요. 호출하는 함수와 호출되는 함수의 시그니처가 호환돼야 하고, 스택에 있는 지역 변수의 주소를 넘기면 안 돼요. 그리고 MSVC는 아직 지원이 없어서 윈도우까지 커버하는 크로스 플랫폼 코드라면 이식성을 고민해야 하고요. 그래도 인터프리터, 상태 머신, 파서처럼 '함수 A가 끝나면 무조건 B로 넘어가는' 구조가 나오는 코드라면 배워둘 가치가 충분해요.

한줄 정리: C에서도 이제 컴파일러가 '보장하는' 꼬리 호출을 쓸 수 있게 됐고, 이건 인터프리터 성능 최적화의 새로운 표준 기법이 되어가고 있어요.

여러분은 인터프리터나 상태 머신 만들 때 디스패치를 어떻게 구현하시나요? computed goto 파와 musttail 파, 어느 쪽이신가요?


🔗 출처: Hacker News

SOURCE · HACKER NEWS
원문 전체 보기 → https://lwn.net/Articles/1034703/
SHARE
처리 중...