backend를 정당화하는 아픔을 찾아서
Mere는 네 backend를 가졌고, 그 하나에는 명확한 일이 없었다. LLVM backend는 C의 것과 겹쳤다: native 코드는 이미 C를 통해 손으로 쓴 C의 속도로 돌았고, web은 WebAssembly로 돌았다. 그래서 짐작으로 지우는 것도 신앙으로 지키는 것도 아니라, 물음은 언어가 모든 것을 답하는 방식으로 답해졌다 —— 그것을 강요하는 구체적 아픔을 찾으러 간다. 세 probe가 찾으러 나갔고, 저마다 정당화를 찾으리라 예기했고, 저마다 녹았다. backend가 필요 없다는 발견은 그 자체로 가질 값이 있고, probe 중 하나가, 찾고 있지 않던 곳에서, 나중에 효할 진실을 캐냈다.
네 backend를 가진 언어는, 저마다가 무엇을 위한지를 말할 수 있어야 한다. Mere의 셋은 쉽게 답했다: interpreter는 빠르게 반복하기 위해, C backend는 손으로 쓴 C의 속도로 native 바이너리를 낳고, WebAssembly는 web 위와 Node 호스트 아래에서 돈다. 넷째, LLVM backend는 답하지 못했다. 그것이 낳는 것을 무엇이든, C backend도 낳고, 꼭 같은 속도로. 그것은 당연한 것으로 실려 왔다, 진지한 언어라면 LLVM을 타겟해야 한다고 가정하는 방식으로, 그리고 조용히 중복이 되어 있었다.
말끔한 선택지는, 그것을 지우거나, 원칙으로 계속 지키거나였다. 어느 쪽도 이 언어가 것을 정하는 방식이 아니다. feature는 실의 현재 아픔을 없앰으로써 그 자리를 얻으므로, 정직한 한 수는, LLVM backend를 genuine히 필요한 것으로 강요하는 아픔을 찾으러 가는 것 —— 그리고 답이 비어 돌아와도 믿는 것, 이었다. 세 probe가 나갔다.
거울이었던 천 배의 gap
첫 probe는 JIT 컴파일러를 원하는 고전적 이유를 쫓았다: runtime에서 코드를 받아, 그것을 빠르게 돌려야 하는 프로그램. 식의 트리를 천만 번 평가하는 데, interpreter로 걸으면 이십 초, 같은 계산을 native 코드로 곧장 쓰면 이십 밀리초 걸렸다. 천 배의 gap은 꼭 JIT 형태의 구멍의 형태다.
그런 뒤 probe가 dogfood되었다, 파일의 각 행에 대해 사용자 공급의 식을 평가하는 awk 풍의 도구로서, 그리고 gap은 무너졌다. 천 배는 이중 해석의 산물이었다 —— Mere interpreter가 그 자신, 식의 트리를 걷기 위해 트리를 걷고 있었다. 도구를 미리, 평범한 방식으로 컴파일하면, 그 식 평가기는 native 코드가 된다; runtime의 식은 그때 본질적으로 native의 속도로 돌고, JIT는 어디에도 없다. 가설에게 더 나쁘게, 이런 종류의 실의 도구는 산술이 아니라 입력과 출력에 묶인다: 이백만 행에 걸쳐, 식이 단일한 열이든 깊게 중첩된 식이든, wall-clock 시간은 움직이지 않았다, 평가가 parse와 print의 노이즈에 묻혔기에. JIT가 누그러뜨리는 아픔은, 이런 종류의 프로그램이 실제로는 갖지 않는 아픔이었다.
하나의 genuine한 균열
둘째 probe는 실의 이유에 가장 가까운 것을 찾았고, 그것은 genuine이고 동시에 작기에 정밀히 진술할 값이 있다. 깊은 상호 말단 재귀 —— 두 함수가 서로를 천만 번 부른다 —— 는 WebAssembly에서 완주하고, 최적화와 함께 컴파일한 native 바이너리에서 완주하고, 최적화 없이 컴파일한 native 바이너리에서 스택 오버플로로 크래시했다. WebAssembly는 format의 수준에서 말단 호출 제거를 보증한다; C는 안 하고, 그것을 C 컴파일러의 optimizer에 맡기고, 그것은 높은 최적화에서 접고 낮은 데서 저버린다. C에는 그 제거를 요구하는 수단이 없다. native 코드에서 말단 호출을 보증하려면, 그것을 할 수 있는 LLVM의 중간 표현을 원하게 될 것이다.
이것이 LLVM이 기대는 한 곳이다 —— 그리고 여기서조차 기댐은 가볍다, 왜냐하면 권장되는 native 빌드는 최적화와 함께 돌고, 거기서는 말단 호출이 접히기에. 보증이 중요해지는 것은 genuine히 그것을 요하는 워크로드를 위해서뿐이다: 계속이 무거운 interpreter, 상태 기계, 영원히 ping-pong하는 스타일로 쓰인 proof checker. 그런 프로그램이 나타나 optimizer의 베스트에포트를 불충분하다 여길 때까지, 그것을 보증 하는 기구는 아무도 묻지 않은 물음에 답하고 있다. 하지만 flag할 값이 있다, 이 probe가 말단 호출과 보증에 대해 무언가 진실을 찾았음을 —— 그리고 그 진실이, probe가 찾고 있던 native 코드가 아니라, 아주 다른 어딘가에서, 나중에 효해 옴을.
C가 표현할 수 없는 것
셋째 probe는 물음 통째를 재구성했고, 그 재구성이 실의 결론이다. 쓰고 있는 C 컴파일러는 그 자신 LLVM 위에 세워져 있다. 그래서 LLVM이 유명한 모든 것 —— 그 optimizer, 그 vectorization, 그 link-time의 일 —— 을, C backend는 이미 공짜로 물려받는다, 그 컴파일러를 통해 컴파일함으로써. LLVM backend는, C backend가 전혀 표현할 수 없는 무언가를 함으로써만, 그 밥값을 벌 수 있었고, 탐색은 그것이 무엇일 수 있는가로 좁혀졌다.
가장 유망해 보인 후보는 coroutine —— 계산을 중단하고 재개하는 능력 —— 이었고, resumable한 effect가
그것을 요구한다는 논리였다. 하지만 Mere의 effect는 평범한 capability 값으로 전달된다; algebraic effect
handler도, resume도, yield도, 중단하는 것은 아무것도 없고, 그래서 수요는 그 방향에서 그냥 생기지
않는다. 그리고 사람이 실제로 원하는 중단의 패턴 —— 지연 스트림, generator —— 은 평범한 closure와
thunk로 표현되고, C를 통해 native로 컴파일되어 아무 일 없이, 유한한 답으로 합쳐진 무한 스트림으로
검증되었다. LLVM이 대체 불능일 genuine한 케이스는, C가 진정 닿지 못하는 것 —— GPU 커널을 emit한다,
tracing 가비지 컬렉터를 돌린다 —— 이고, 그 저마다가 언어가 무엇인가의 변경, Mere가 하지 않은 전환이다.
그 어느 것도 현재의 아픔이 아니다.
부정의 값
그래서 답은 비어 돌아왔고, 빔이 가질 값이 있는 답이었다. LLVM backend를 존재로 강요하는 아픔은 없었다, 왜냐하면 은밀히 LLVM인 C 컴파일러가 이미 LLVM의 최적화를 전하고, WebAssembly가 이미 말단 호출을 보증하고, effect 시스템은 중단하지 않기에. backend는 있는 곳에 머문다 —— 실험적, 당분간은 중복 —— 그리고 그것에 투자하지 않는다는 결정은 어깨를 으쓱함이 아니라, 재어진 발견이고, 그 트리거는 이름 지어져 있다: Mere가 GPU 코드를 emit하고 싶어지는 날, 혹은 그 effect가 재개하는 능력을 길러 zero-allocation한 상태 기계로 컴파일하고 싶어지는 날, probe는 다른 결과로 돌아올 것이다.
이것은 이전 Part가 락과 atomic을 세우기를 거절했을 때 실천한 것과 같은 절제다: 세우지 않음을, 세움과 꼭 같은 만큼, 증거를 요하는 행위로 다루는 규율. 세 probe의 성과는 한 문장이었다 —— LLVM에 아직 투자하지 말라, 그리고 이것이 그 이유다 —— 취향이 아니라 계측에 뒷받침되어. 그리고 그 부정의 안에 접혀 있던 것은, 보증된 말단 호출에 대한 하나의 작은 진실로, 지나가며 눈치채이고, 넣어 두어지고, 컴파일러가 자기 자신을 컴파일하려는 이야기의 Part를 기다리고 있었다.