# Rust arena로 Gleam의 3년 묵은 성능 이슈 해결하기

> Clean Markdown view of GeekNews topic #31398. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=31398](https://news.hada.io/topic?id=31398)
- GeekNews Markdown: [https://news.hada.io/topic/31398.md](https://news.hada.io/topic/31398.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-13T23:02:43+09:00
- Updated: 2026-07-13T23:02:43+09:00
- Original source: [giacomocavalieri.me](https://giacomocavalieri.me/writing/gleam-rust-arenas)
- Points: 1
- Comments: 1

## Topic Body

- Gleam 포매터의 재귀적 `Document` 구조에서 반복되던 **개별 힙 할당**을 arena 기반 참조로 바꿔, 3년간 남아 있던 성능 개선 이슈를 해결함
- `Box&lt;Self&gt;`를 arena에 저장된 `&Self`로 교체하고, Rust의 **수명 검사**로 arena가 제거된 뒤 내부 데이터가 참조되지 않도록 보장함
- 언어 키워드와 쉼표처럼 반복되는 `Document` 수백 개를 **한 번만 할당해 재사용**하면서 프리티 프린터 실행 시간이 13ms에서 9.8ms로 줄어 24% 빨라짐
- 소스 읽기와 파싱을 포함한 전체 `gleam format` 실행 시간도 **13% 단축**됐으며, 최대 메모리 사용량은 8.4MB에서 7.6MB로 약 10% 감소함
- arena를 전달하도록 함수와 호출부를 광범위하게 고쳐야 해 `+2963/-1032` 규모의 수작업이 필요했지만, **반복적인 개별 할당을 줄이는 방식**으로 속도와 메모리 사용량을 함께 개선할 수 있었음

---

### 재귀적 `Document` 구조에서 발생한 할당 비용
- [Gleam](https://gleam.run/)의 프리티 프린터는 코드를 어떻게 출력하고, 줄 제한을 넘을 때 어디서 나눌지를 나타내는 재귀적 **`Document` 자료구조**를 사용함
- 주요 variant는 출력할 문자열을 담는 `String`, 줄바꿈 전후 문자열을 정의하는 `Break`, 여러 문서를 묶는 `Group`, 줄바꿈 시 들여쓰기를 늘리는 `Nest` 등으로 구성됨
- 리스트 `[1, 2]`도 `String`, `Break`, `Nest`, `Group`을 조합해 표현함
  - 현재 줄에 들어가면 `[1, 2]`처럼 한 줄로 출력함
  - 줄 제한을 넘으면 지정된 `Break`에 따라 여러 줄로 나누고 들여쓰며 마지막 쉼표도 출력함
- `Nest`처럼 다른 `Document`를 내부에 담는 variant는 기존에 `Box&lt;Self&gt;`를 사용했기 때문에 중첩 문서마다 **힙에 개별 할당**해야 했고, 이 작업이 실행 시간의 상당 부분을 차지할 수 있었음
- Gleam 프리티 프린터의 기반 알고리듬은 [Strictly Pretty](https://lindig.github.io/papers/strictly-pretty-2000.pdf) 논문에서 확인할 수 있음

### `Box`를 arena 참조로 교체
- 기존 `Nest(Box&lt;Self&gt;)`를 `Nest(&'doc Self)`로 바꿔 arena에 저장된 다른 `Document`를 **참조**하도록 구조를 변경함
- 참조를 담기 위해 문자열 수명 `'string`과 별도로 문서 데이터의 수명 `'doc`을 `Document`에 추가함
- 구현에는 [`typed_arena`](https://docs.rs/typed-arena/latest/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`에 넣어야 했던 [수백 개의 작은 문서](https://github.com/gleam-lang/gleam/pull/5824/changes#diff-fd77bddd869b2a4e981c876948b9c61e195184ef23a2becbc4ac023aed51f920R590)를 각각 한 번만 할당할 수 있게 됨

### 단순한 API와 광범위한 호출부 변경
- arena API 자체는 단순하지만, 기존 코드의 큰 부분을 전환하는 과정에는 반복적인 수정이 필요했음
- 이전에 `Box::new`만 호출하던 함수들은 이제 데이터를 할당할 **arena 인수**를 추가로 받아야 함
- `format_list` 같은 함수는 목록 항목을 포맷하는 하위 함수에도 같은 arena를 전달하고, 중첩 문서를 `Box::new` 대신 `arena.alloc`으로 생성하도록 바뀜
- 며칠간 신중하게 검색·치환한 끝에 [`+2963/-1032` 규모의 pull request](https://github.com/gleam-lang/gleam/pull/5824)로 작업을 완료함
- 반복적이고 수동적인 작업이었으며, 이 과정에서 LLM은 사용하지 않았음

### 속도와 최대 메모리 사용량 개선
- 실제 Gleam 프로젝트인 [`squirrel`](https://github.com/giacomocavalieri/squirrel)을 포맷할 때 프리티 프린터 실행 시간이 **13ms에서 9.8ms**로 감소함
  - 프리티 프린터만 비교하면 **24% 성능 향상**임
- `gleam format`은 포맷 외에도 프로젝트 소스 코드를 읽고 파싱하므로 프리티 프린터가 전체 작업의 일부만 차지하지만, 전체 실행 시간도 **13% 단축**됨
- 최대 메모리 사용량은 **8.4MB에서 7.6MB**로 줄어 약 10% 감소함
- arena로 개별 힙 할당 횟수를 줄인 결과, 포매터의 실행 속도가 빨라지고 메모리 사용량도 함께 감소함

## Comments



### Comment 61736

- Author: neo
- Created: 2026-07-13T23:02:44+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/7840ca/closing_three_year_old_issue_using_rust) 
- 최고 메모리 사용량을 10% 더 줄이고 싶다면 참조 대신 **불투명 핸들**을 반환하는 다른 아레나를 고려할 만함. 핸들을 64비트 대신 16비트나 32비트로 만들 수 있어 작은 할당이 많을 때 효과가 큼  
  https://donsz.nl/blog/arenas/ 에 따르면 이를 지원하는 크레이트는 [`compact_arena`](https://crates.io/crates/compact_arena)뿐이지만, 요구에 맞지 않으면 직접 구현하기도 어렵지 않음
  - 불투명 핸들을 쓰면 데이터를 더 조밀하게 저장할 수도 있음. 개인 프로젝트인 [columnar](https://github.com/frankmcsherry/columnar)는 `Option&lt;T&gt;` 목록을 항목당 1비트 남짓의 존재 여부와 실제로 존재하는 항목의 `T`로 나눠 저장함  
    대신 참조형으로 `&Option&lt;T&gt;`가 아니라 `Option<&T>`를 반환하며, 더 일반적으로는 구조적 재귀 안에 참조를 두기보다 참조를 따라 구조적으로 재귀하는 방식임
  - 해당 글은 **2년 전 조사**를 바탕으로 했고 모든 크레이트를 완벽히 다루지는 못했으니, 오류나 빠진 새 라이브러리를 발견하면 PR을 보내주면 됨  
    아레나는 더 널리 쓰일 가치가 있으며, 핸들이나 스마트 포인터 대신 참조를 반환하면 Rust에서 **재귀 열거형 패턴 매칭**이 매우 편해짐. 보통 `if let MyAst::Add(MyAst::Multiply(a, b), c) = expr {}`처럼 쓰려 해도 중간의 박스나 포인터 때문에 불가능하지만, 참조는 nightly 기능 없이 투명하게 패턴 매칭할 수 있음. 모든 참조가 한 아레나를 가리키면 트리 전체를 연결하는 수명도 일관되게 유지됨
  - 결과가 좋아서 컴파일러의 다른 부분도 **아레나 방식으로 전환**하는 방안을 고려 중이며, 이 접근을 계속 살펴볼 예정임

- 이 방식은 또 다른 [Wadler 방식](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf)의 예쁜 출력 라이브러리인 [`pretty`](https://crates.io/crates/pretty)가 지원하는 접근과 꽤 비슷함. 다만 여러 **메모리 할당 방식**을 지원하다 보니 API가 다소 부담스럽게 느껴질 수 있음

- 글에서 인용한 [논문](https://lindig.github.io/papers/strictly-pretty-2000.pdf)의 저자가 Brunswick의 평범한 IT 회사에서 일한다니 놀라움
  - 이것이 **오픈소스**의 묘미임. 전문성과 생업은 서로 큰 상관이 없는 경우가 많음
