Rust arena로 Gleam의 3년 묵은 성능 이슈 해결하기
10 hours ago
3
- Gleam 포매터의 재귀적 Document 구조에서 반복되던 개별 힙 할당을 arena 기반 참조로 바꿔, 3년간 남아 있던 성능 개선 이슈를 해결함
- Box<Self>를 arena에 저장된 &Self로 교체하고, Rust의 수명 검사로 arena가 제거된 뒤 내부 데이터가 참조되지 않도록 보장함
- 언어 키워드와 쉼표처럼 반복되는 Document 수백 개를 한 번만 할당해 재사용하면서 프리티 프린터 실행 시간이 13ms에서 9.8ms로 줄어 24% 빨라짐
- 소스 읽기와 파싱을 포함한 전체 gleam format 실행 시간도 13% 단축됐으며, 최대 메모리 사용량은 8.4MB에서 7.6MB로 약 10% 감소함
- arena를 전달하도록 함수와 호출부를 광범위하게 고쳐야 해 +2963/-1032 규모의 수작업이 필요했지만, 반복적인 개별 할당을 줄이는 방식으로 속도와 메모리 사용량을 함께 개선할 수 있었음
재귀적 Document 구조에서 발생한 할당 비용
- Gleam의 프리티 프린터는 코드를 어떻게 출력하고, 줄 제한을 넘을 때 어디서 나눌지를 나타내는 재귀적 Document 자료구조를 사용함
- 주요 variant는 출력할 문자열을 담는 String, 줄바꿈 전후 문자열을 정의하는 Break, 여러 문서를 묶는 Group, 줄바꿈 시 들여쓰기를 늘리는 Nest 등으로 구성됨
- 리스트 [1, 2]도 String, Break, Nest, Group을 조합해 표현함
- 현재 줄에 들어가면 [1, 2]처럼 한 줄로 출력함
- 줄 제한을 넘으면 지정된 Break에 따라 여러 줄로 나누고 들여쓰며 마지막 쉼표도 출력함
- Nest처럼 다른 Document를 내부에 담는 variant는 기존에 Box<Self>를 사용했기 때문에 중첩 문서마다 힙에 개별 할당해야 했고, 이 작업이 실행 시간의 상당 부분을 차지할 수 있었음
- Gleam 프리티 프린터의 기반 알고리듬은 Strictly Pretty 논문에서 확인할 수 있음
Box를 arena 참조로 교체
- 기존 Nest(Box<Self>)를 Nest(&'doc Self)로 바꿔 arena에 저장된 다른 Document를 참조하도록 구조를 변경함
- 참조를 담기 위해 문자열 수명 'string과 별도로 문서 데이터의 수명 'doc을 Document에 추가함
- 구현에는 typed_arena 크레이트를 사용함
- alloc으로 값을 arena에 저장하면 해당 값의 참조가 반환됨
- 할당된 데이터는 arena가 살아 있는 동안 사용할 수 있음
- arena가 범위를 벗어나면 내부 데이터도 함께 제거됨
- alloc(&self, value: T) -> &mut T가 반환하는 참조는 arena보다 오래 살 수 없으며, Rust의 대여 검사기(borrow checker) 가 arena 제거 후의 잘못된 참조를 차단함
반복되는 문서를 한 번만 할당해 재사용
- arena는 다른 문서를 참조하는 용도뿐 아니라 코드 전반에서 반복되는 Document를 캐시하는 데도 활용됨
- String("fn"), String("pub"), String("type")처럼 언어 키워드를 나타내는 문서를 매번 새로 만들 필요가 없어짐
- 목록 항목 사이에 쓰는 Break { unbroken: ", ", broken: "," } 같은 문서도 한 번 할당한 뒤 재사용함
- 기존에는 계속 Box에 넣어야 했던 수백 개의 작은 문서를 각각 한 번만 할당할 수 있게 됨
단순한 API와 광범위한 호출부 변경
- arena API 자체는 단순하지만, 기존 코드의 큰 부분을 전환하는 과정에는 반복적인 수정이 필요했음
- 이전에 Box::new만 호출하던 함수들은 이제 데이터를 할당할 arena 인수를 추가로 받아야 함
- format_list 같은 함수는 목록 항목을 포맷하는 하위 함수에도 같은 arena를 전달하고, 중첩 문서를 Box::new 대신 arena.alloc으로 생성하도록 바뀜
- 며칠간 신중하게 검색·치환한 끝에 +2963/-1032 규모의 pull request로 작업을 완료함
- 반복적이고 수동적인 작업이었으며, 이 과정에서 LLM은 사용하지 않았음
속도와 최대 메모리 사용량 개선
- 실제 Gleam 프로젝트인 squirrel을 포맷할 때 프리티 프린터 실행 시간이 13ms에서 9.8ms로 감소함
- gleam format은 포맷 외에도 프로젝트 소스 코드를 읽고 파싱하므로 프리티 프린터가 전체 작업의 일부만 차지하지만, 전체 실행 시간도 13% 단축됨
- 최대 메모리 사용량은 8.4MB에서 7.6MB로 줄어 약 10% 감소함
- arena로 개별 힙 할당 횟수를 줄인 결과, 포매터의 실행 속도가 빨라지고 메모리 사용량도 함께 감소함
-
Homepage
-
Tech blog
- Rust arena로 Gleam의 3년 묵은 성능 이슈 해결하기