언어를 재는 한 줄 루프
메모리 수정을 설계하기 전에, 규율은 숫자를 요구했다. probe는 「할당을 멈추지 않는 가장 단순한 프로그램」= 스트림의 줄 세기다. 그것은 애초에 쓸 수 없었다 —— read_line은 interpreter에만 있었다. 쓸 수 있게 되자, 측정은 누구의 이야기와도 일치하기를 거부했다: string 판은 줄마다 30 바이트가 영구 잔류했고, 검약일 터인 byte 버퍼 판은 10배 나빴다. 그 이상을 쫓자, 컴파일러 역사상 가장 파급이 큰 성능 버그에 다다랐다 —— 그리고 수정 후, 세는 루프는 wc보다 야위어 있었다.
서버의 메모리 문제는 thread와 channel과 store에 얽혀 있어서, 수정을 생각하기에는 움직이는 부품이
너무 많았다. 방법은 말한다: 단리하라. 같은 병을 가진 최소의 프로그램은, 줄을 읽고, 보고, 잊는 단일
스레드 루프 —— 언어 자신으로 쓴 wc -l에 수백만 줄을 먹이고, 상주 메모리만을 출력으로 본다.
per-item 회수가 언젠가 성립한다면, 먼저 여기서 성립해야 한다. 그리고 probe는 첫 측정 전에, 낯익은
형태로 일을 했다: 언어에는 줄 읽기 builtin이 있었지만, interpreter에만 —— native 프로그램이 계속
발견하는 interpreter-only 일가의 여섯째다. read_stdin은 설계부터 전량 읽기. C backend가
read_line을 배울 때까지, native의 스트리밍 줄 읽기는 애초에 쓸 수 없었다. 작은 릴리스를 하나
끼우고, 측정이 시작되었다.
두 방언, 두 개의 틀린 숫자
string 방언의 카운터 —— 관용적인 코드, read_line과 str_len —— 는 예상대로의 병상을 보였다:
상주 메모리가 입력 크기를 따라가고, 줄마다 약 30 바이트의 영구 잔류. wc가 2.5 메가바이트로 세는
800만 줄에, 4분의 1 기가바이트. 모든 줄 문자열이 program-lifetime region에 착지해 눌러앉는다. 이
숫자가, 언젠가 올 수정이 심판받을 기준선이 되었다. 대조 실험은 언어를 돋보이게 할 터였다: byte 방언
—— tcp_read로 하나의 재사용 버퍼에 읽고, mem_get_u8로 개행을 세고, 문자열을 일절 만들지 않는다
—— 은 상수 메모리로 달릴 터였다. 실제로는 2 기가바이트로 달렸다. 검약판이 낭비판의 10배 나쁘다.
언어에 대해 누군가 믿고 있는 어떤 이야기도 이것을 설명할 수 없었다. 측정이란 이를 위해 있다: 모두의
모델과 모순되는 숫자는, 버그의 현주소다.
모든 호출이 집세를 내고 있었다
이상의 이분 탐색은, 아무도 본 적 없는 곳으로 이끌었다. 그것이 언어에서 가장 흔한 코드의 형태 안에
숨어 있었기 때문이다. ML의 다인자 함수는 단일 인자 함수의 연쇄이고, C backend는 모든 호출을 단마다
컴파일하고 있었다: count buf i n acc는 「count에 buf를 건네고, closure를 받고, 거기에 i를 먹이고,
closure를 받고……」가 되고 —— 그 한 단마다 closure 환경을 program-lifetime region에, region의 thread
lock 너머로, 매 호출 할당하고 있었다. 3억 바이트를 도는 루프 속 4인자 함수는, 바이트마다 환경을
셋씩, 영구히 쌓는다. 모든 dogfood의 모든 핫 루프가 이 집세를 보이지 않게 내고 있었다. byte 방언
probe는 그것을 3억 번 연속으로 냈을 뿐이다. 수정은 고전 그 자체 —— top-level 커링 함수마다 직접
N 인자 C 함수도 emit하고, 딱 포화된 호출을 거기로 직결한다. C의 인자 평가순은 미규정이므로, 임시
변수로 interpreter의 왼쪽에서 오른쪽을 고정한다. 부분 적용은 closure 연쇄인 채. 관측 가능한 것은
아무것도 바뀌지 않는다. 바뀌는 것은 계량기다: 환경 제로, lock 통행 제로, 그리고 자기 재귀는 진짜
C 꼬리 호출이 된다.
wc보다 야위고, 그리고 정직한 분할
uncurrying이 들어가자, byte 방언 카운터는 3억 바이트를 0.1초에, 1.5 메가바이트・입력 크기와 무관한
상수 메모리로 처리했다 —— wc 자신보다 아래다. 숫자는 기분 좋지만, 진단 쪽이 중요했다. string
방언을 다시 재니, 전과 거의 같은 4분의 1 기가바이트. 그것이야말로 요점이고, 두 문제가 깨끗이
분리되었다. 하나는 사고 —— 모든 포화 호출에 말없이 과세하던 컴파일 전략으로, 이것으로 영구히
폐지되었다. 다른 하나가 진짜 설계 부채 —— 관용적인 string 코드에는 per-line 메모리를 돌려줄 방법이
여전히 없다. 영구한 것은 호출 규약이 아니라 값 그 자체이기 때문이다. probe는 probe의 일을 했다 ——
「서버가 어떻게든 leak한다」를 두 개의 정밀한 문장으로 바꿨고, 하나는 이미 고쳐졌고, 하나는 이제
피할 수 없게 되었다. 다음 회는 그 피할 수 없는 쪽에 착수한다 —— 다만 그 전에 샛길이 하나: 메모리
공사의 단계를 밟는 사이, generic한 데이터 구조가, interpreter와 컴파일러가 프로그램의 의미에 대해
항상 일치하지는 않음을 조용히 실연해 보인 것이다.