압축기·컴파일러처럼 출력이 비자명한 소프트웨어에서 산책하듯 결과와 내부 상태를 둘러보며 직관을 키우고 버그와 개선점을 찾는 방법을, Compiler Explorer 같은 도구와 실제 사례를 통해 소개합니다.
밖을 산책하는 건 몸과 마음에 좋다.[citation needed] 나무 사이를 느긋하게 걷다 보면 마음속 소란이 잦아들고 복잡한 엔지니어링 문제가 사라지기도 한다.
Vicki Boykis는 좀 더 비유적인 산책에 대해 쓴 글, Walking around the app을 올렸다. 그는 프로덕션 애플리케이션의 인터페이스를 늘 직접 사용해 보면서, 전체가 결합감 있게 설계되어 날카로운 모서리가 적은지 확인하라고 말한다.
그는 애플리케이션의 구현 다른 부분들도 산책하듯 둘러보며, 불일치, 과도하게 복잡한 장치, 깨진 빌드를 고치는 일에 대해서도 이야기한다. 등산길에서 남이 버린 쓰레기를 주워 담는 것과 비슷하다.
정말 훌륭하고 거의 모든 소프트웨어 프로젝트에 두루 통하는 조언이다. 그러다 보니 내가 컴파일러를 어떻게 산책하듯 둘러보는지 생각하게 되었다.
데이터를 변환하는 부류의 소프트웨어 프로젝트—압축 라이브러리, 컴파일러, 검색 엔진—에는 또 다른 층위의 “산책”이 가능하다. 코드만 있는 게 아니라, _비자명한 출력_도 있기 때문이다.
여기서 비자명하다는 건, JSON 응답처럼 반쯤 규칙적인 결과가 아니라 어떤 품질 축을 따라 스케일하는 출력을 뜻한다. 압축이라면 크기, 컴파일러라면 생성된 코드가 그렇다.
이미 몇 가지 생성된 케이스를 테스트로 코드베이스에 체크인해 두었을 가능성이 크다. 훌륭하다. 나는 골든 테스트(golden test)가 정확성 보장에도, 이해를 돕는 측면에서도 아주 좋다고 생각한다. 하지만 이런 고립된 이해는 더 복잡한 예제로 확장되지 않을 수 있다.
예컨대 당신의 컴파일러는 루프 안의 switch-case 문을 어떻게 처리하나? 기대한 대로 점프 스레딩(jump threading)을 해 주나? 쿠키를 베어 물며 문득 궁금해졌을 수도 있고, 어쩌면 옵티마이저 코드를 스크롤하다가야 떠올랐을 생각일 수도 있다.
당신이 CF Bolz-Tereick라고 치고, PyPy의 IR을 넘겨보고 있다고 하자. 다음처럼 생긴 IR이 눈에 들어온다:
v0 = ...
v1 = float_abs v0
...
v2 = float_abs v1
...
v3 = float_abs v2
“흠”, 당신은 속으로 말한다. “float_abs 연산은 양수를 낸다는 걸 옵티마이저가 당연히 추론할 수 있어야 하지 않을까?”
하지만 옵티마이저의 어떤 사소한 특이점 때문에 그렇지 않다. 예전에는 됐는데 지금은 안 될 수도 있고, 애초에 한 번도 안 됐을 수도 있다. 어쨌든 이런 가벼운 산책이 한 줄짜리 수정으로 고칠 수 있는 버그를 드러냈다:
def return_type(op):
match op:
case float_abs(_):
- return float
+ return float.with_range(low=0, high=None)
이제 다행히 IR이 훨씬 말끔해졌다:
v0 = ...
v1 = float_abs v0
...
...
그리고 이걸 단정한 테스트 케이스로 체크인해 둘 수 있다.
재미있는 사실: 이게 내가 PyPy 프로젝트를 처음 접한 계기였다. CF가 ECOOP 2022 현장에서 이 버그를 고치는 과정을1 라이브로 안내해 주었다! 정말 즐거웠다.
가정(그리고 나중에는 테스트)을 확인하기가 까다롭다면, 그것은 당신의 라이브러리가 개발자에게 내부 상태를 충분히 드러내지 않는다는 신호일 수 있다. 이는 곧 가정이나 의심을 즉시 점검하지 못하게 만드는 사용성 장애로 이어진다.
훌륭한 영감을 얻고 싶다면 Kate의 프로그램 내부에 대한 트윗을 보라.
설령 콘솔로 출력하는 --zjit-dump-hir 같은 플래그가 있더라도, 휴대폰2이나 친구 컴퓨터에서는 실행하기 어려울 수 있다. 그런 경우에는 _더 친절한 도구_가 필요할지도 모른다.
올바른 종류의 도구는 탐구를 부른다.
Matthew Godbolt는 내가 처음 쓴, 친절한 컴파일러 탐색 도구를 만들었다. 바로 Compiler Explorer (“Godbolt”)다. 브라우저에서 여러 언어로 프로그램을 입력하면 즉시 컴파일 결과를 보여 준다. 합리적인 범위 내에서 프로그램을 실행해 주기도 한다.
이 도구는 강력하다:
이 조합은 확인 작업의 장벽을 엄청나게 낮춘다.
이제 때로는 반대가 필요하다. 흐름을 끊지 않도록 터미널이나 에디터 안에서 쓸 수 있는, Compiler Explorer 같은 것 말이다. 안타깝게도 나는 아직 견줄 만한 도구를 찾지 못했다.
이런 도구를 쓰면 특정 입력과 출력을 수시로 점검할 수 있는 즉각적 효과 외에도, 장기적으로 컴파일러 동작에 대한 직관이 쌓인다. 이는 곧 _mechanical sympathy_를 길러 준다.
나는 대학원 지원서 목적 진술서 (PDF)와 인터넷에 올린 짧은 글 몇 편을 제외하고는 mechanical sympathy에 대해 많이 쓰지 않았다. 당장은 그 정도로만 남겨 두겠다.
당신의 컴파일러는 아마 어떤 애플리케이션들을 컴파일하고 있을 것이고, 그 애플리케이션의 함수들에 대한 IR에 접근할 수도 있을 것이다.
모든 함수의 최적화된 IR을 스크롤해 보라. 너무 많다면 상위 N개의 함수 IR만이라도 보자. 무엇을 개선할 수 있을지 살펴보라. 예상치 못한 패턴을 보게 될지도 모른다. 5월에는 아무것도 눈에 띄지 않더라도, 8월쯤에는 컴파일러의 발전이나 그 사이에 읽은 멋진 논문 덕분에 시야가 달라질 수 있다.
나는 한 번, IR에서 “immortal”로 표시되어야 할 객체들이 실제로는 참조 카운팅(refcount)되고 있는 걸 보고 copy-on-write(COW)와 잠재적 메모리 문제를 야기하던 기묘한 참조 카운팅 버그를 찾아낸 적이 있다. 버그는 컴파일러에 있지 않고 애플리케이션 초기화 코드 어딘가 멀리 있었지만, IR에는 드러나 있었다.
결론은 Vicki의 조언과 비슷하다.
도구에 애정을 쏟으세요. 동료들이 알아챌 것이다. 사용자들도 알아챌 것이다. 심지어 당신의 기분도 좋아질지 모른다.
이 글에 피드백을 준 CF에게 감사드린다.