The Goose Programming Language
Goose 프로그래밍 언어: 힙 없이 메모리 안전성을 보장하는 시스템 언어
Goose는 힙 할당이나 가비지 컬렉션 없이, 컴파일러가 정적으로 관리하는 데이터 스택에 값을 저장하는 시스템 언어입니다. 벤치마크에서 C++와 안전한 Rust보다 빠르고 메모리를 적게 쓴다고 주장하지만, 데이터 수명에 맞춰 구조를 설계해야 하며 재현 가능한 비교에는 의문이 남습니다.
- 주제
에디터 노트
Goose의 성능 주장은 데이터 배치와 수명 관리라는 언어 설계의 이점에 기대지만, 저장소의 비교 코드가 손 최적화 버전이 아니라 미끼라는 작성자의 답변은 수치의 재현성을 크게 떨어뜨립니다. 결과표와 측정 방식이 있어도 독자가 비교 기준을 검증하기 어렵습니다. 반면 설계의 범위와 비용은 구체적으로 드러납니다. 임의 수명의 가변 데이터를 풀로 옮기거나 내부 배열의 크기를 제한해야 하며, 공유 상태가 필요한 병렬 작업도 큐를 거쳐야 합니다. 작성자는 메모리 안전성과 UB 부재를 명시했지만, 새 언어의 명세와 컴파일러 검증이 실제 사용에서 그 보장을 얼마나 뒷받침하는지는 별도 검토 대상입니다. AI가 구현 대부분을 맡았다는 사실 자체보다, 명세와 테스트, 벤치마크를 독립적으로 확인할 수 있는지가 채택 판단의 근거가 됩니다.
AI 요약
Goose는 값의 수명을 어휘적 범위에 맞춰 관리하고, 동적 데이터를 컴파일러가 정한 데이터 스택에 쌓는 시스템 언어입니다. 힙 할당, 가비지 컬렉션, 참조 카운팅, 소멸자를 쓰지 않습니다. 스코프가 끝나면 스택 포인터를 되돌려 메모리를 정리합니다. 컴파일러는 참조의 수명을 추론해 소유자보다 오래 살아남는 참조를 거부하며, 개발자가 lifetime annotation을 작성하지 않아도 메모리 안전성을 보장한다고 설명합니다.
메모리 모델과 자료 구조
동적으로 크기가 바뀌는 최상위 배열은 주소 공간을 예약한 데이터 스택에서 자랍니다. 배열이 커져도 기존 원소를 옮기지 않으므로 그 원소를 가리키는 참조가 유지됩니다. 컴파일러는 동시에 살아 있는 가변 배열을 서로 다른 스택에 배치합니다. 배열 안에 배열을 넣을 때는 내부 배열을 생성 후 고정하거나, 용량을 정해 두거나, 풀(pool)을 쓰는 식으로 구성해야 합니다.
배열과 문자열, 가변 크기 필드를 가진 구조체는 포인터로 별도 영역을 가리키지 않고 데이터 안에 연속해서 놓입니다. 예를 들어 Goose의 Order 레코드는 29바이트와 할당 0회라고 제시합니다. 같은 구조를 C++의 string과 vector로 만들면 160바이트와 할당 1회, Rust에서는 153바이트와 할당 4회라고 비교합니다. 가변 크기 원소 배열은 순회할 수 있지만 직접 인덱싱할 수 없습니다. 별도 인덱스 배열을 두는 방법은 있습니다.
열거형도 저장 방식을 선택합니다. 고정 모드는 가장 큰 변형에 맞춘 공간을 확보해 인덱싱과 덮어쓰기를 지원합니다. 가변 모드는 각 원소가 실제 변형 크기만 차지하지만 순차 접근만 가능하고 원소의 변형을 바꿀 수 없습니다. 상대 참조는 대상까지의 거리를 1, 2, 4바이트 오프셋으로 저장할 수 있습니다. 이렇게 만든 데이터 구조는 위치가 바뀌어도 참조가 유지되며, 파일에 기록한 뒤 읽을 때 태그와 길이, 링크를 검증할 수 있습니다.
언어 기능과 제약
함수 반환값은 호출자의 최종 저장 위치에 바로 생성됩니다. 따라서 가변 배열을 값으로 반환해도 임시 배열을 만든 뒤 복사하지 않는다고 설명합니다. 오류는 return err from load 문법으로 여러 호출 프레임을 건너뛰어 반환할 수 있습니다. 컴파일러가 경로를 정적으로 확인하며, 중간 함수의 일반 반환형은 바뀌지 않습니다.
제네릭과 고차 함수는 모노모피제이션 방식으로 처리합니다. 함수값은 컴파일 시점에 결정되므로 함수 포인터나 클로저 객체를 만들지 않는다고 합니다. 스레드 함수는 별도 메모리를 쓰며, 평탄한 값만 타입이 지정된 큐로 복사해 주고받습니다. 런타임 내부에는 원자 연산이 있지만 개발자가 직접 잠금이나 메모리 순서를 관리하지 않는다는 설명입니다. 그 대가로 공유 가변 상태와 동적 디스패치, 함수 포인터, 이스케이프 클로저를 지원하지 않습니다. 재귀 호출 사이에 살아남아야 하는 가변 데이터도 별도 풀로 전달해야 합니다.
성능과 구현 상태
저장소의 16개 벤치마크 기하 평균에서 Goose는 MSVC 기준 관용적 C++보다 3.51배, 손으로 최적화한 C++보다 1.24배, 가장 빠른 안전한 Rust보다 1.13배 빠르다고 제시합니다. 피크 메모리는 각각 1.95배, 1.32배, 1.25배 적습니다. clang 기준 속도는 3.54배, 1.26배, 1.19배입니다. 측정은 종료 과정까지 포함한 전체 프로그램 실행 시간입니다. 작성자는 결과와 손실 사례도 공개한다고 안내합니다.
언어는 C++로 작성한 약 3만 줄 규모의 전체 프로그램 컴파일러이며 C 코드 한 파일을 생성합니다. TinyCC를 사용한 프로세스 내 실행 경로와 C 컴파일러를 이용한 빌드 경로가 있습니다. Windows, macOS, Linux에서 테스트하며 Apache 2.0 라이선스를 적용합니다. 작성자는 AI가 구현과 문서 작성 대부분을 맡았고, 자신이 설계한 언어 명세와 기존 Lobster 컴파일러의 구조를 바탕으로 작업했다고 밝힙니다.
Hacker News 반응
- @Aardappel — 컴파일러는 현재 범위 안에서 자랄 수 있는 배열을 데이터 스택에 자동 배치합니다. 프로그래머가 데이터를 어디에 저장할지 정하며, 보통은 비슷한 수명을 가진 데이터를 함께 둡니다. 순서와 무관하게 해제해야 할 때는 배열을 풀로 바꿀 수 있습니다.
- @Aardappel — 배열의 배열은 스택 하나에 저장합니다. 내부 배열은 생성 시 크기를 정하거나 고정 크기 또는 용량 제한 배열로 만들 수 있고, 필요하면 다른 배열에 풀을 둘 수 있습니다.
- @malisper — 크기가 제각각인 문자열을 해시 테이블에 무작위로 넣고 지우는 구조를 어떻게 표현하는지 궁금합니다. 문자열을 한 스택에 모은 뒤 주기적으로 압축하는 방식을 떠올렸습니다.
- @Aardappel — 데이터 수명이 비슷한 구조를 배열에 함께 두면 가장 큰 이점을 얻습니다. 임의로 노드를 삭제해야 한다면 고정 용량 배열이나 단일 원소 또는 연속 영역을 위한 풀 같은 도구를 쓸 수 있습니다.
- @Aardappel — 스레드 간 복사는 런타임이 처리하며, 런타임은 내부적으로 원자 연산을 씁니다. 작업자는 프로세스가 아니라 스레드입니다. C++와 Rust보다 빠르다는 말은 평균에 관한 것이며, 더 느린 알고리즘 사례도 있습니다.
- @pixelesque — 저장소의 C++ 벤치마크 코드에는 reserve 호출이 거의 없습니다. 손으로 최적화한 C++와 Rust 코드를 따로 공개했는지 궁금합니다.
- @Aardappel — 실제 손 최적화 파일은 비공개이며 저장소의 파일은 미끼입니다.
- @Aardappel — Goose에는 unsafe 기능이 없고 정의되지 않은 동작도 없으며 메모리 안전성을 보장합니다. 성능의 원천이기도 한 데이터 배치 위치를 개발자가 생각해야 하는 점은 사용성의 부담입니다.
- @adastra22 — AI가 쓴 문서로는 언어 설계 중 인간이 의도한 부분과 AI가 만들어낸 부분을 구분하기 어렵습니다. 프로젝트가 어디로 발전할지 판단하려면 설계자의 설명이 더 필요합니다.
- @Aardappel — 기존 컴파일러의 코드 스타일을 분석해 새 컴파일러에 적용했습니다. 익숙한 구조를 따라가면 검토하기 쉽고, 기존 코드의 맥락 안에서 AI가 더 나은 코드를 작성한다고 봅니다.
- @Aardappel — Goose는 64비트 주소 공간이 실용화된 점을 활용합니다. 큰 주소 공간을 배열에 예약하면 재할당 없이 자랄 수 있어 포인터 안정성과 다른 설계 요소를 얻습니다.
원문: Hacker News / 번역·요약: Trawling