threads 에 어떤 개발자분이 Bun의 Garbage Collection 동작 차이를 테스트 하고 계시다는 글을 보고 따라해보았다.
Bun 이 node.js 보다 적극적으로 가비지 컬렉팅에 개입하는데, 함수의 실행이 완료되거나 스코프의 끝에서 스코프 전환이 일어나기 전에 컬렉팅을 진행한다. 반면 node.js 는 메모리 할당량이 특정 임계값에 도달하지 않으면 가비지 컬렉션을 하지 않는다.
let testObj = { name: 'kim' }
const weakRefObj = new WeakRef(testObj)
console.log('normal: ', weakRefObj.deref()) // 정상 출력
testObj = null // 여기서 testObj 는 GC 가 수집할 대상이 된다.
// 스코프 전환
setTimeout(() => {
console.log('scope changed: ', weakRefObj.deref())
}, 0)
WeakRef 는 입력받은 오브젝트의 참조를 반환하는 클래스이다. .deref() 메서드는 참조하는 오브젝트를 반환하는데 만약 해당 오브젝트가 없다면(가비지 컬렉팅 되었다면) undefined 를 반환한다.
setTimeout 으로 스코프가 전환될 때 node.js 는 가비지 컬렉션을 실행하지 않아 참조하는 오브젝트가 나오는 반면 bun 은 가비지 컬렉션을 실행해 undefined 가 나오는 것을 볼 수 있다.
~/Documents/test-bun ❯ bun test-bun-gc.js
normal: {
name: "kim",
}
scope changed: undefined
~/Documents/test-bun ❯ node test-bun-gc.js
normal: { name: 'kim' }
scope changed: { name: 'kim' }
bun 은 오브젝트 출력하면 들여쓰기도 해준다. 역시 bun이 최고다.
사용되지 않는 오브젝트는 바로 메모리에서 할당 해제 해버리는 모습을 볼 수 있다.
Bun은 공식문서의 title 에다가도 "Bun — A fast all-in-one JavaScript runtime" 이라고 적어놓을 만큼 속도에 자신감을 보이고 있다.
그런데 node.js 보다 더 적극적으로 가비지 컬렉션을 진행하면 당연히 node.js 보다 느려야 하지 않을까? 라는 의심이 들었다. 가비지 컬렉션을 배울 때 컬렉팅을 위해 애플리케이션 또는 스레드의 실행을 멈추게 되며 "Stop the world" 가 발생한다고 배웠기 때문이다.
찾아본 결과 bun 의 목표 중 하나는 GC 일시 중지 시간(GC pause time)을 최소화 하는 것이라고 한다. 짧고 잦은 GC 가 한 번에 많은 작업을 처리하는 길고 드문 GC 보다 사용자가 느끼는 지연 시간(latency) 을 줄여준다는 것.
그리고 bun 은 zig 라는 언어로 작성되었는데(node.js 는 C++로 작성되어 있다) zig 는 rust 만큼은 아니지만, C++ 보다는 강력한 메모리 통제권을 제공한다.
힙 할당에 대한 제어권을 프로그래머에게 완전히 넘겨 개발자가 메모리 할당 및 해제 시점을 정확히 예측하고 런타임을 튜닝하기 매우 유리하다. 또한 use-after-free 같은 메모리 오류를 디버그 모드에서 즉시 감지할 수 있어 안전성을 확보하면서도 고성능 코드를 작성하기 용이하다고 한다.