4P by GN⁺ | ★ favorite | 댓글 1개
  • 커널과 드라이버처럼 성능과 동시성이 중요한 코드에서도 메모리 해제는 단순한 free() 호출로 끝나지 않으며, 락 없는 공유 기법이 필요해질 수 있음
  • RCU(Read, Copy, Update) 는 자주 읽히고 드물게 바뀌는 데이터를 복사한 뒤 포인터를 원자적으로 교체해, 읽기 경로를 막지 않는 방식임
  • 이전 버전을 즉시 delete하면 아직 읽는 스레드에서 use-after-free가 발생할 수 있어, RCU는 읽기 구간을 추적하고 안전한 시점까지 해제를 늦춤
  • RCU는 Linux에서 수만 번 쓰이고 Folly C++ 라이브러리와 Rust의 crossbeam-epoch에도 있으며, 사용 여부에 따라 나중에 정리된다는 점에서 GC와 같은 형태를 가짐
  • 수동 메모리 관리가 항상 더 빠르고 예측 가능하다는 이분법은 약하며, free(), 참조 카운팅, OS 메모리 동작도 비용과 불확실성을 갖기 때문에 현대 GC도 시스템 프로그래밍의 도구가 될 수 있음

커널 코드가 RCU 같은 기법을 쓰는 이유

  • 운영체제는 매일 실행되는 프로그램 중에서도 성능 민감도가 높은 축에 속함
  • OS가 빨라지면 사용자가 더 많은 계산을 할 수 있으므로, 커널과 드라이버 개발자는 코드 최적화에 많은 노력을 들임
  • 운영체제는 사용자 공간의 프로세스와 스레드뿐 아니라 커널 자체의 여러 스레드, 하드웨어 인터럽트 핸들러까지 다뤄야 함
  • 대기 시간이 늘어나면 사용자의 시간을 빼앗기 때문에, 커널 코드에서는 락 없이 스레드 간 데이터를 공유하는 여러 기법이 등장함

RCU의 기본 동작

  • RCU(Read, Copy, Update) 는 매우 자주 읽히지만 드물게 쓰이는 데이터에 맞는 방식임
    • 현재 연결된 USB 장치 집합처럼 거의 바뀌지 않지만 바뀔 수는 있는 데이터가 예가 됨
    • 변경은 원자적으로 일어나야 하며, 이미 읽고 있는 독자를 막지 않아야 함
  • 작성자는 공유 상태를 다음 순서로 갱신함
    • 기존 데이터를 포인터에서 읽음
    • 기존 데이터를 복사하고 필요한 변경을 적용해 새 버전을 만듦
    • 포인터를 원자적으로 갱신해 새 버전을 가리키게 함
  • 독자는 공유 포인터를 읽기만 하므로, 읽기 경로는 단순하고 대기 없이 동작함
  • 이 방식은 사용하기 쉽고 wait-free이지만, 이전 버전을 정리하지 않으면 메모리 누수가 발생함

즉시 해제할 수 없는 이전 버전

  • 포인터를 새 버전으로 바꾼 직후 이전 버전을 바로 delete하면 use-after-free 위험이 생김
  • 락 없이 동작하기 때문에 작성자는 아직 이전 버전을 읽고 있는 독자가 있는지 알 수 없음
  • 독자는 rcu_read_lock()rcu_read_unlock()으로 읽기 측 임계 구역을 표시할 수 있음
    • 독자는 여전히 블록되지 않음
    • 작성자는 해당 독자들이 빠져나갈 때까지 이전 데이터를 제거하지 않음
  • rcu_synchronize()는 모든 독자가 사라질 때까지 기다리는 것이 아니라, 이전 버전을 볼 가능성이 있는 이전 독자가 끝날 때까지만 기다리면 됨
    • 새 포인터를 본 독자는 새 버전을 사용하므로 이전 버전의 수명과 무관함

지연 해제와 GC의 형태

  • 작성자가 갱신 함수 안에서 기다리지 않아도, 이전 데이터가 언젠가 안전하게 해제되면 코드는 올바르게 동작함
  • rcu_defer(old) 같은 방식은 현재 독자가 임계 구역을 벗어난 뒤 아무 때나 old를 해제할 수 있게 함
  • 전용 스레드가 오래된 미참조 버전을 주기적으로 정리하는 형태는 세대별 GC와 닮아 있음
  • RCU는 사고 실험이 아니라 실제로 널리 쓰이는 기법임
    • Linux는 RCU를 수만 번 사용함
    • Facebook의 Folly C++ library에 RCU가 제공됨
    • Rust에서는 crossbeam-epoch라는 이름으로 쓰이며, 인기 있는 동시성 라이브러리의 기반이 됨
  • RCU가 “진짜 GC”인지 따지는 분류 논쟁보다 중요한 점은, 메모리가 사용 중인지 여부에 따라 나중에 정리되는 구조가 GC와 같다는 데 있음

수동 해제의 숨은 비용

  • GC가 수동 메모리 관리보다 본질적으로 덜 효율적이라는 통념은 구현 세부를 보면 약해짐
  • free()는 공짜가 아님

    • 범용 메모리 할당자는 커널에서 받은 페이지, 크기별 버킷 분할, 사용 중인 버킷 같은 내부 전역 상태를 관리해야 함
    • 여러 스레드가 할당자 상태를 잠그려 하면서 경합이 생길 수 있음
    • jemalloc처럼 스레드 로컬 풀을 두더라도 이를 동기화하기 위한 추가 코드가 필요함
  • RAII와 lifetime도 할당자 비용을 없애지는 못함

    • Rust의 lifetime이나 C++의 RAII는 메모리 해제 자동화와 정확성에는 도움을 주지만, 할당자 내부 구조의 복잡성을 없애지는 않음
    • 많은 시나리오에서는 shared_ptrArc로 돌아가야 함
    • 참조 카운트라는 추가 메타데이터가 필요하고, 이 값이 코어와 캐시 사이를 오가며 비용을 만들 수 있음
    • 라이브니스 그래프의 순환을 누수시킬 수도 있음
  • GC가 제공할 수 있는 최적화도 있음

    • 이동식 세대별 GC는 힙을 주기적으로 다시 압축함
    • 할당은 포인터 증가에 가까워져 높은 처리량을 낼 수 있음
    • 순차 할당의 지역성이 좋아져 캐시 성능에도 도움이 됨

메모리 관리 제어에 대한 착각

  • GC에 반대하는 많은 개발자는 소프트 실시간 시스템을 만들고 있음
    • 비디오 게임의 FPS나 스트리밍 코덱의 압축 성능처럼 최대한 빠른 동작을 원함
    • 하지만 가끔 1밀리초가 더 걸린다고 시스템이 깨지거나 사람이 죽는 하드 지연 요구는 아님
  • 프로그래머가 메모리 관리가 언제 일어나는지 결정할 수 있다는 믿음은 단순하지 않음
    • 운영체제는 하드웨어 상호작용을 추상화함
    • Linux는 기본적으로 메모리 요청 시 거의 아무것도 하지 않고, 실제로 사용하려 할 때 메모리를 내줄 수 있음
    • madvise(), 메모리 매핑 I/O, 파일 시스템 캐시가 섞이면 “무엇이 언제 할당됐는가”에 대한 단순한 답이 없음
    • 나쁜 날에는 단순한 포인터 접근이 디스크 I/O로 이어질 수도 있음
  • 프로그래머가 메모리 관리를 위해 멈추기 좋은 시점을 항상 안다는 믿음도 제한적임
    • 비디오 게임 로딩 화면처럼 명확한 경우도 있음
    • 많은 소프트웨어에서는 더 중요한 작업으로 바쁘지 않을 때가 유일한 답임
    • shared_ptrArc를 가진 개별 코드는 자신이 마지막 소유자가 되어 정리를 떠맡을지 미리 알 수 없음
  • free() 호출이 곧 메모리를 OS에 돌려준다는 믿음도 항상 맞지 않음
    • 메모리는 페이지 단위로 OS에서 할당됨
    • 할당자는 프로그램 종료 전까지 페이지를 붙잡고 재사용하려는 경우가 많음
    • OS는 스왑을 통해 페이지를 회수할 수도 있음

GC를 시스템 프로그래밍 도구로 볼 수 있는 이유

  • 모든 소프트웨어가 GC의 혜택을 받는 것은 아님
  • 하지만 2024년에 가까운 시점에도 시스템 프로그래머 사이에서 GC 논의는 잘못된 이분법과 공포·불확실성·의심에 묻히기 쉬움
  • GC를 쓰는 언어가 수동 메모리 관리 언어보다 “명백히” 느리다는 생각은 사실이 아니라 이데올로기에 가깝다고 봄
  • 생명이 걸린 시스템을 만드는 팀에서도, 거의 모든 줄에서 할당하는 GC 언어로 서브 마이크로초 지연을 제공한 사례가 있음
  • 시스템의 일부가 반드시 n 클록 사이클 안에 실행돼야 한다면, 그 특정 부분만 비-GC 코드나 하드웨어로 분리할 수 있음
  • GC는 만능 해결책이 아니지만, 두려워하지 않고 사용할 수 있는 도구 상자 속 도구 중 하나임

댓글과 토론

Hacker News 의견들
  • 유망한 현대적 병렬 가비지 컬렉션 기법으로는 MPL 또는 MaPLe와 그 새로운 Automatic Management of Parallelism를 볼 만함
    POPL 2024 distinguished paper award와 ACM SIGPLAN dissertation award 2023을 받은 핵심은 두 가지임: a) disentanglement 기반의 증명 가능한 효율적 병렬 가비지 컬렉션, b) 증명 가능한 효율적 자동 세분성 제어
    [1] MaPLe (MPL): https://github.com/MPLLang/mpl
    [2] Automatic Parallelism Management: https://dl.acm.org/doi/10.1145/3632880

    • Standard ML과 그 커뮤니티는 메모리 관리 문헌에 꽤 인상적인 기여를 해왔음
      링크한 논문도 있고, 영역 기반 메모리 관리의 초기 사용자이자 선구자 중 하나였던 MLKit도 있음
    • 여기서 “증명 가능한 효율적”이라는 게 무슨 뜻인지 궁금함
    • 이 접근이 OCaml에서 최근 진행된 멀티코어 병렬성 지원 작업과 어떻게 비교되는지 궁금함
    • 더 잘 아는 사람들에게 묻고 싶은데, 이게 다른 언어에도 얼마나 적용 가능할까?
      예를 들어 이런 접근이 Go의 가비지 컬렉션을 크게 빠르게 만들 수 있을까, 아니면 기존 언어 설계상의 문제에 부딪히게 될까?
  • RCU 사용 사례는 설득력 있지만, 다른 상황에서의 가비지 컬렉션 경험은 좋지 않았음
    이 글은 정적 수명이 동적 수명보다 일반적으로 낫다는 주장이라기보다, 맞춤형 메모리 관리 해법이 최고 성능을 낼 수 있다는 주장처럼 읽힘
    free()가 메모리를 운영체제에 돌려준다고 믿는 게 아니라, 할당자에게 돌려준다고 봄. 이게 운영체제에 돌려주는 것보다 훨씬 낫고, 시스템 호출은 느림. 다만 mimalloc처럼 해제된 메모리를 다음 malloc에 즉시 쓰지 않고 주기적으로만 가능하게 만들 수도 있음
    800바이트를 할당한 뒤 즉시 free하는 일을 100만 번 반복하고 고유 포인터 수를 세면 glibc malloc은 1, jemalloc은 1, mimalloc은 4, Julia 가비지 컬렉터는 62767이 나왔음
    62767개, 약 48MiB라 아주 나쁘진 않지만 그래도 내 컴퓨터의 L3 캐시를 밀어냄. 가비지 컬렉션을 쓰면 새 할당이 캐시가 아니라 RAM에서 오는 것이 거의 보장되고, 할당이 많은 코드 성능을 망침. 메모리 관리 자체의 속도뿐 아니라 그것이 준 메모리로 얼마나 빨리 작업할 수 있는지도 중요함
    Julia에서 이를 보여주는 벤치마크를 올렸음: https://discourse.julialang.org/t/blog-post-rust-vs-julia-in...
    malloc/free는 실제 작업 메모리가 충분히 작다면 캐시에 뜨겁게 남을 기회를 줌. mimalloc 같은 할당자도 압축 가비지 컬렉션처럼 연속 할당이 가깝게 배치되도록 설계되어 있고, 내가 본 mimalloc의 4개 고유 포인터는 896바이트 간격이었음
    압축 가비지 컬렉션 경험이 더 많았다면 생각이 덜 냉소적이었을 수도 있지만, 가비지 컬렉션은 안전한 메모리 관리를 위해 Rust의 빌림 검사기 같은 것보다 훨씬 복잡한 해법이라고 봄. 그 복잡성이 컴파일러와 런타임 개발자에게 떠넘겨지므로 사용자는 보통 괜찮고, 성능 민감하지 않은 코드를 쓸 때는 받아들일 만한 절충임. 정적 수명을 가진 RAII도 더 맞춤형 접근이 필요하지 않은 코드에는 합리적인 절충이고, 글의 예시는 분명 맞춤형 해법이 필요한 경우임

    • 가비지 컬렉션이 더 많은 포인터를 줬으니 캐시 지역성이 나쁘다고 말하는 것만으로는 부족함
      압축 가비지 컬렉션은 장기 실행 프로그램에서 힙 단편화가 TLB 캐시 항목과 객체 사이 여유 공간을 낭비하므로, 거의 항상 malloc보다 캐시 활용이 좋음. 압축 가비지 컬렉션의 범프 할당자는 free가 메모리를 회수하지 않으니 할당마다 새 포인터를 주지만, 그 할당들은 순차적이고 힙을 계속 소모하면서 가장 최근 객체만 만지는 경우라면 여전히 캐시에 남아 있음. 할당자와 가비지 컬렉터의 파급 효과를 벤치마크하는 건 극도로 어렵고, 이런 합성 벤치마크는 거의 다 회의적으로 봄
    • 글은 왜 이 방식이 RCU 맥락에서는 잘 작동하는지, 왜 일반적으로는 별로인지 설명한 뒤 그냥 제쳐두고 무시함
      사람들이 걱정하는 건 메모리를 더 이상 쓰지 않는지 알아내는 과정이, 리소스 사용을 끝냈다고 할당자에게 직접 말하는 것보다 비효율적이고 비결정적이라는 점임. 해제를 미루는 것 자체를 걱정하는 사람은 본 적이 없음
      전체 살아있는 집합을 순회하는 일은 드물고, 30년 동안 가비지 컬렉션 알고리즘을 개선해 거의 지각이 있는 수준까지 왔지만, 이 문장은 사람들이 실제로 문제 삼는 지점을 의도적으로든 아니든 무시함. 서비스에서 가비지 컬렉션 문제가 나면 여기저기 튜닝해서 분노한 영혼을 그림자 세계로 돌려보내길 바라는 주술사를 불러야 하는 느낌임
      쓰레기를 표시하고 더 이상 쓰이지 않을 때 알림을 받는 방식이라면 그 과정 전체가 사라짐. 가비지 컬렉션에서 메모리 할당이 매우 빠를 수는 있지만, 공정하게 비교하려면 표시와 압축 비용도 상각해서 포함해야 함
      또 하나의 큰 문제는 같은 성능을 얻기 위해 일반적으로 가비지 컬렉션이 수동 메모리 관리보다 훨씬 더 많은 메모리를 요구한다는 점임. 참조가 아직 있는지 반복해서 중복 확인할 여분의 CPU도 필요하고, 낙관적 압축을 위한 추가 메모리 복사도 감수해야 함
      마지막으로 글은 수명이 불명확할 때 Rust의 Arc/Rc 같은 수동 메모리 관리가 필요하다고 비판하지만, 가비지 컬렉션 언어에서도 finalizer가 반드시 호출된다고 믿을 수 없어서 외부 리소스를 닫기 위해 사실상 똑같은 인프라를 만든다는 점은 무시함
      이 논쟁은 지난 20~30년 동안 충분히 반복됐고, 이 글은 정당한 가비지 컬렉션 우려를 밈으로 무시하는 것 외에 새로 가져오는 게 없어 보임. 밈은 재미있으니 괜찮지만, 정답은 일반적 정답이 없다는 것임. 시스템의 설계 제약을 만족시키는 데 맞는 도구를 쓰면 됨
    • 이 주장은 잘 이해되지 않음. 세대별 가비지 컬렉션에서는 0세대가 캐시에 있을 가능성이 높고, 바로 거기서 대부분의 생성·폐기가 일어남
      그 밖의 더 오래 사는 할당은 정의상 캐시 관점에서 제어하기 쉽지 않음. 지역성은 가비지 컬렉션의 큰 장점 중 하나이고, 내가 아는 유일한 문제는 전체 중단 표시/쓸기임. 현대 가비지 컬렉션이 백그라운드 스레드를 둔다는 건 알지만, 그래도 전체 중단 이벤트는 생기는 것으로 앎
    • free()가 메모리를 할당자에게 돌려준다는 건 맞지만, 장기 실행 서버에서 메모리 단편화를 다루는 건 전혀 즐겁지 않음
      특히 slab 할당자가 관리하는 페이지의 내부 단편화가 그렇고, 흔한 문제는 아니지만 다루기 어려운 문제임
    • 캐시 사용이 그렇게 큰 관심사라면 아레나 할당은 수동 메모리 할당에서와 마찬가지로 잘 동작함
      다행히 가비지 컬렉션이 이렇게 편리하게 꾸며진 예시와 경쟁해야 하는 영역은 많지 않음
  • 모든 메모리를 쉽게 아레나로 처리할 수 있는 특수한 경우를 제외하면, 좋은 추적 가비지 컬렉션은 처리량에서 오래전에 수동 메모리 관리를 넘어섰고, 최근에는 지연 시간 영향도 대다수 애플리케이션에서 충분히 받아들일 만함
    OpenJDK의 ZGC는 일반적인 중단 시간이 두세 자리 마이크로초 단위이고, 합리적인 할당률에서는 최악도 1ms를 거의 넘지 않으며, 운영체제가 유발하는 중단과 비슷한 범위임
    진짜 중요한 절충은 메모리 사용량뿐임. 특수한 틈새, 즉 아레나가 모든 것에 잘 맞고 최악 지연 시간이 낮은 마이크로초 범위인 경우를 빼면 핵심 질문은 하나임. 내 애플리케이션이 메모리 제약 환경에서 실행되는가, 또는 RAM 사용량을 줄이기 위해 다른 것을 희생할 가치가 있는가

    • 내 경험으로는 반대임. 객체별 개별 수명은 드문 특수 사례이고, 현실 코드 대부분에는 같거나 아주 비슷한 수명을 가진 관련 객체가 많음
      그런 코드에서 개별 객체 수명을 추적하는 건 과함. 결국 메모리 관리는 수명에 관한 것이고, 많은 개별 수명보다 적은 개별 수명이 항상 더 낫다. 수동이든 자동이든 해야 할 일이 줄기 때문임
      객체 수명을 생각하지 않아도 되는 건 매우 편리하고, 그래서 좋은 가비지 컬렉터의 내부 복잡성이 상당한데도 가비지 컬렉션 언어가 성공했음
    • 중단 시간은 어느 정도 해결됐지만, 가비지 컬렉션의 CPU 사용량은 여전히 꽤 높음
      예측하기 어려운 꼬리 지연 시간과 여러 예외 상황의 영향도 계속 받게 됨
    • 직장에서 ZGCShenandoah를 벤치마크 중인데, p100 중단 시간은 보통 500us 아래임
      ZGC가 Shenandoah보다 중단 횟수가 적어 보이고, 그래서 중단 한 번당 더 많은 일을 하는 듯해 조금 더 나은 성능을 보임
      아직 운영 환경 테스트를 해야 하지만, 지금까지는 ZGC, 그리고 Java 21 이후의 세대별 ZGC를 쓰면 가비지 컬렉션 중단은 대체로 해결된 문제처럼 보임
    • 모든 메모리를 쉽게 아레나로 처리할 수 있어야 한다는 기준은 불공정해 보임. 대부분의 객체가 아레나로 쉽게 할당된다면, 그것만으로도 가비지 컬렉션 필요성의 대부분이 사라짐
      Jai의 관점처럼 메모리 할당은 흔한 순서대로 네 가지로 나눌 수 있음: 1) 극도로 짧게 살아 함수 스택에 둘 수 있는 것, 2) 짧게 살고 수명이 잘 정의되어 프레임/요청 단위 메모리 아레나에 둘 수 있는 것, 3) 오래 살고 소유자가 잘 정의되어 하위 시스템 전용 풀로 관리할 수 있는 것, 4) 오래 살고 소유자가 불명확해 동적 메모리 관리가 필요한 것
      추적 가비지 컬렉션이 일반적으로 수동 메모리 관리를 앞선다고 주장하려면, 여기저기서 malloc/free를 호출하는 시스템이 아니라 이런 관점을 염두에 두고 작성된 시스템과 비교해야 함. 현대 C++/Rust 관행과 비교하면 더 공정할 수 있음
      대부분의 시스템에서는 추적 가비지 컬렉션에 의존하는 편이 훨씬 실용적일 가능성이 높다는 데는 동의하지만, 그건 전혀 다른 주장임
    • 근거가 필요함
  • 글은 RCU에 동기를 부여한 뒤 유턴해서 범용 가비지 컬렉션을 일반적으로 옹호하기 시작함
    트로이 목마까지는 아니지만 꽤 급격한 전환처럼 느껴짐

    • RCU를 가비지 컬렉션이라고 부르지는 않겠음. 어느 시점에도 객체가 쓰레기인 상태가 아니기 때문임
      객체는 세 상태 중 하나에 있고 가능한 한 빨리 전환됨: active, obsolete but alive for old readers, deallocated
      코드 작성 방식에 따라 “obsolete-but-alive” 객체를 “new” 할당에 안전하게 재사용할 수도 있을 듯하지만, 성능은 완전히 분석해보지 않았음
      가비지 컬렉션 논의에서 흔하듯, 언제 shared_ptr/Arc로 “후퇴”해야 하는지가 매우 모호함. 실제로는 참조 카운트를 피하는 것, 즉 이미 소유권이 있음을 증명하거나 간접 참조를 아예 피하는 것이 진지한 참조 카운트 기반 시스템의 핵심임. 아무것도 하지 않는 것이 가비지 컬렉션의 “언젠가 뭔가 하기”보다 당연히 더 나음
  • 내가 쓰는 소프트웨어에서는 두 경우가 있음. (1) 항상 맞춤형 할당자를 쓰고 할당을 피할 뜨거운 경로, (2) 그 밖의 모든 것
    (1)에서는 가비지 컬렉션이든 아니든 차이가 없고, 나는 빠져나갈 것임. (2)에서는 가비지 컬렉션이 정말 편하고 올바름

    • 동의함. Java/C++를 같이 쓰던 곳에서 이 양분법을 상호 운용으로 해결하려 했지만, 결국 해결한 것보다 문제가 더 많았음
      Java가 현대적 가비지 컬렉터로 해온 작업은 인상적이지만, 그들조차 Valhalla를 통해 간접적으로 무할당/저할당 코드의 자리가 있음을 인정하고 있음
  • 현대 사용자용 운영체제, 즉 특수한 RTOS가 아닌 운영체제에는 내장 가비지 컬렉션이 있다는 관찰이 여기서 살짝 비껴감
    우리는 그냥 그렇게 부르지 않고 메모리 관리라고 부를 뿐임. 내장 가비지 컬렉션이 있는 언어를 뭐라고 부르는가? 메모리 관리 언어라고 부름
    오래된 “위에서 아래로” 실행되는 C 프로그램에서 이런 모습을 자주 봄. 할당하고, 시스템 리소스를 정리하지만 free는 신경 쓰지 않음. 프로그램이 종료되면 운영체제가 그 메모리를 전부 되찾으니 왜 굳이 하겠나
    여기에는 프로그램과 덜 격리된 운영체제 수준의 가비지 컬렉터, 언어 런타임 가비지 컬렉터처럼 리소스를 다루는 운영체제를 만들 기회가 있음. 하지만 보통 가비지 컬렉션 언어에서는 가비지 컬렉터가 런타임 거의 모든 줄에 복잡하게 얽혀 있으므로, 한 운영체제용 배포판만 그 제어를 운영체제에 넘기도록 만드는 건 실용적이지 않음
    그래도 아쉬움. 프로그램 수준 메모리 관리와 운영체제 수준 메모리 관리의 인위적 격리 때문에 생기는 만성 문제를 개선할 여지가 많기 때문임

    • 프로그램 종료 시 운영체제가 처리해주므로 메모리뿐 아니라 파일, 소켓, 스레드 같은 다른 리소스도 굳이 해제할 필요는 없음. AmigaOS가 아니라면 그렇다
      메모리를 해제하는 유일한 이유는 장기 실행 애플리케이션에서 운영체제에서 새 메모리를 더 가져오지 않고 다른 할당에 재사용하기 위해서임. 한 번 실행하고 끝나는 명령줄 도구에서는 보통 필요하지 않음
    • 운영체제는 프로세스가 종료될 때에만 메모리를 해제할 수 있다는 걸 앎. 파일 핸들이나 다른 리소스도 마찬가지임
      프로세스가 일을 끝내면 종료되도록 설계되어 있다면 운영체제를 가비지 컬렉터처럼 쓸 수 있음
      하지만 실행 중인 프로그램 내부에서 어떤 메모리가 사용되지 않는지 운영체제가 아는 방식은 존재한 적이 없음. 난해한 연구용 운영체제 정도는 예외일 수 있음. 그래서 놓친 기회라기보다는, 놓쳤다고 하는 대상이 의미 있는 형태로 존재하지 않는다고 봄
      반면 아주 단순하고 짧게 사는 프로그램을 쓰는 프로그래밍 방식은 완전히 정당함. CLI 도구와 그것을 스크립트하는 스크립트 언어가 그렇게 동작하고, 예전 웹 서버도 CGI 등으로 그렇게 동작했으며, 오늘날에도 충분히 합리적인 접근임
    • Java의 Epsilon GC가 바로 그런 것임
  • (1) RCU에서 범용 추적 가비지 컬렉션으로 넘어가는 전환은 미끼를 던져놓고 바꿔치기하는 느낌임
    (2) 수동 메모리 관리는 malloc/free 호출만이 아니라 배치에 관한 것임. 예를 들면 구조체 배열 분리, 인라이닝, 암시적 오프셋, 패킹 등이 있음

    • (2)에 대해, Virgil에는 다양한 수준으로 메모리 배치를 제어할 수 있는 기능이 여러 가지 있음
      “구조체의 배열”을 말한 것으로 보이는데, 튜플 배열로 할 수 있고 대상에 따라 자연스럽게 평탄화·정규화됨. 즉 네이티브 대상에서는 구조체의 배열이 됨
      바이트 단위로 정확한 배치도 정의할 수 있고[1], 주로 다른 소프트웨어와의 연동이나 이진 형식 파싱에 쓰임. 대수적 자료형을 언박싱할 수 있으며, 곧 대수적 자료형의 정확한 인코딩까지 제어할 수 있음
      Virgil은 가비지 컬렉션을 사용함
      [1] https://github.com/titzer/virgil/blob/master/doc/tutorial/La...
    • 2번이 수동 메모리 관리라는 데는 동의하지 않음
      현대 관리형 언어에 메모리 배치 제어가 부족한 건 분명하지만, 저수준 언어에서도 완벽과는 거리가 멀고, 영향을 줄 방법도 분명히 있음
  • 이 글에서 빠진 것 하나는 async/await가 가비지 컬렉션과 아주 잘 맞는다는 점임
    개인적인 특이한 스타일 이유로 async/await를 싫어하지만 길게 말하진 않겠음
    TypeScript/JavaScript에서 많이 써봤고 Dart에서도 써봤으며, 거기서는 기대한 대로 동작함
    Rust에서도 써봤는데, 내 생각에는 재앙임. 다중 스레드 런타임에서 async/await를 쓰기 위해 필요한 식의 메모리 관리를 억지로 끼워 넣는 건 지옥도임
    https://doc.rust-lang.org/std/pin/index.html

    • Rust의 일반적인 비동기 코드에서는 pinning이 필요하지 않음. 이건 라이브러리 작성자를 위한 것임
  • 가비지 컬렉션을 옹호하는 많은 글에서 빠지는 지점이 있는데, 이 글도 보기에 그렇듯 메모리는 리소스의 한 종류일 뿐임
    특히 시스템 프로그래밍에서 올바른 코드는 파일 핸들, 소켓 등 외부 리소스도 관리해야 함. 가비지 컬렉션은 애플리케이션 메모리 부분만 해결하므로 이런 외부 리소스 처리에는 전혀 도움이 되지 않음. 오히려 훨씬 복잡하게 만들 수 있고, .NET에서 사소하지 않은 IDisposable을 올바르게 구현하려면 무엇이 필요한지만 봐도 알 수 있음
    RAII나 참조 카운트 같은 접근은 내 경험상 메모리와 외부 리소스를 통합된 방식으로 다루기 훨씬 쉽게 만들어서, 올바른 코드를 쓰고 추론하기도 쉬워짐
    그렇다고 가비지 컬렉션에 노골적으로 반대하는 건 아님. 다른 모든 것처럼 장단점이 있는 도구임. 글에서 말한 “수동 GC” RCU 접근은 특정 작업에는 흥미로움

    • 메모리와 다른 리소스 사이에는 큰 차이가 있음. 메모리는 처리 능력과 마찬가지로 모든 계산의 근본 요소임
      대부분의 이론적 계산 모델이 무한 메모리를 가정하는 것도 괜한 일이 아님. 운영체제 커널이나 하드 실시간 애플리케이션 같은 일부 소프트웨어에서는 처리 능력도 수동 할당하지만, 처리 능력을 수동 할당하도록 요구하는 언어는 거의 없음
      비슷한 이유로 자동 메모리 관리는 계산을 추상화하는 데 매우 유용함. 서브루틴의 메모리 세부사항을 호출자에게 새어나가지 않게 하는 것이며, CPU 사용량에서도 이런 세부사항은 드물게만 드러남
      모든 사소하지 않은 계산은 어떤 비상수 양의 처리와 메모리를 포함하지만, I/O는 보통 시스템의 가장자리에서 일어남. I/O 관리도 물론 매우 중요하지만, 계산이라는 개념과 계산 추상화의 중심성에서는 처리와 메모리만큼 핵심적이지 않음
    • 맞고, 메모리 안전성 주장은 다른 리소스에도 적용됨
      예를 들어 Rust는 결국 I/O 안전성을 갖추게 되었고, 그래서 Unix의 OwnedFd 같은 파일 핸들이나 Windows의 OwnedHandle 같은 핸들이 숫자 4 같은 정수가 아니라 소유되는 객체가 됨
      겉으로는 핸들에 산술 연산을 하거나 예약 값을 센티널로 잘못 쓰는 어리석은 실수를 피하는 이야기처럼 보이지만, 소유권 모델 덕분에 핸들로 까다로운 작업을 하더라도 명시적 소유권이 생기고, 이후 유지보수자에게도 투명해짐
    • C++에서 주로 C#으로 옮기니 가비지 컬렉션 메모리 관리는 마음에 들지만, 파일 핸들·소켓 등을 추적하는 건 싫음
      RAII의 가치를 정말 크게 느꼈음
    • 가비지 컬렉션 언어를 비판할 때 자주 놓치는 점은, 그중 대부분이 결정적 리소스 관리를 위한 기능을 갖고 있는데도 많은 사람이 배우지 않는다는 것임
      어떤 언어는 RAII가 있고, 어떤 언어는 키워드를 제공하며, 어떤 언어는 아레나 같은 관리나 암시적 관리를 가진 람다를 제공함. 어떤 언어는 타입 시스템의 도움을 조금 받고, 어떤 언어는 위의 것들을 조금씩 섞음
      게다가 시스템 개발자가 정적 분석기에 의존해야 하듯, 그런 언어의 정적 분석기도 타입 시스템만으로 충분하지 않을 때 빠뜨린 것을 검증해줄 수 있음
    • RAII는 물론 훌륭하지만, 가비지 컬렉션과 제대로 된 예외 처리가 있는 언어라면 리소스를 안전하게 다룰 수 있음
      예를 들어 Java의 try-with-resources 문은 예외가 발생해도 리소스가 안전하게 해제된다는 것을 보장함: https://docs.oracle.com/javase/tutorial/essential/exceptions...
      이런 기본 구성 요소로도 꽤 탄탄하고 리소스 안전한 시스템을 만들 수 있음