시스템 프로그래밍에서 메모리 안전성과 성능은 오랫동안 맞바꿔야 하는 것으로 여겨져 왔다. C와 C++는 수동 메모리 관리로 속도를 얻는 대신 댕글링 포인터와 버퍼 오버런의 위험을 떠안았고, 러스트(Rust)는 소유권과 수명(lifetime) 규칙으로 안전성을 확보했지만 개발자가 익혀야 할 규칙의 벽이 높다. 구스(Goose)는 이 구도에 다른 답을 제시하는 언어다. 힙 할당자도, 가비지 컬렉터도, 수명 애너테이션도 없이 메모리 안전을 보장한다고 내세운다. 공개된 저장소의 벤치마크 기준으로는 C++와 러스트보다 빠르면서도 메모리를 더 적게 쓴다고 주장한다.
힙을 없앤 데이터 스택 모델
구스의 핵심은 힙 자체를 두지 않는다는 데 있다. 모든 동적 값은 컴파일러가 관리하는 '데이터 스택' 위에 인라인으로 올라간다. 프로그램은 네이티브 호출 스택과 정적 데이터, 그리고 컴파일러가 개수(N)를 스스로 계산하는 N개의 데이터 스택을 갖는다. 각 데이터 스택은 커다란 주소 공간을 예약해 둔 영역에 범프 포인터(bump pointer)를 얹은 구조이며, 그 외의 메모리는 존재하지 않는다. 값이 커지면 포인터를 앞으로 밀 뿐이고, 스코프가 끝나면 메모리가 해제된다.
여기서 중요한 제약이자 설계의 묘는, 한 스택에 크기가 변하는 값은 최대 하나만 살아 있을 수 있고 그 값은 항상 맨 위에 있다는 점이다. 덕분에 값이 자라도 무엇인가가 이동하는 일이 없고 용량을 검사할 필요도 없다. 이 모든 조건은 컴파일 타임에 증명되며, 런타임은 범프 포인터만 추적한다. 러스트의 Vec이나 C++의 vector는 재할당 과정에서 원소가 이동할 수 있어 기존 참조가 무효화될 수 있지만, 구스의 push는 새 원소에 대한 참조를 돌려주기 때문에 배열을 만들면서 원소끼리 링크를 걸 수 있다. 문서는 이 '안정적인 참조'가 벤치마크 성능 향상의 여러 요인 중 하나라고 설명한다.
수명은 '루트'로, 데이터는 인라인으로
수명 관리는 애너테이션 대신 '루트(root)'라는 개념으로 처리된다. 모든 참조와 슬라이스에는 대상의 수명을 한정하는 변수, 즉 정적 루트가 있다. 참조는 자신의 대상을 소유한 변수보다 오래 살아남을 수 없고, 호환되지 않는 타입으로 그 대상에 접근할 수도 없다. 컴파일러가 루트를 추론해 함수를 특수화하므로 개발자가 직접 표기할 일이 없으며, 별칭(aliasing)이나 배타성(exclusivity) 규칙도 없다. 수명 검사가 실패하면 오류 메시지가 해당 참조의 루트와 목적지를 짚어 준다.
배열에는 단일한 타입이 없고, 크기가 어떻게 다뤄지는지에 따라 갈라지는 하나의 '계열'이 있다. 모든 구성원은 '[메타데이터][원소들...]' 형태로 인라인·패킹되며, 원소 블록을 가리키는 포인터가 아니다. 문자열도 u8 배열일 뿐이어서, 빌더(u8[>..]), 완성된 문자열(u8[]), 구조체 안에 들어가는 작은 문자열(u8[..16]), 뷰(u8[:])로 나뉜다. 구조체는 가변 크기 부분을 선언 순서대로 인라인으로 품는다. 문서가 든 예시에서 하나의 주문(order) 레코드는 29바이트에 할당 0회로 표현되는데, 같은 것을 C++의 std::string과 std::vector로 짜면 160바이트에 할당 1회, 러스트의 String과 Vec으로 짜면 153바이트에 할당 4회가 든다. 대신 치러야 할 대가는, 가변 크기 원소로 이뤄진 배열은 순차적(sequential)이라 순회는 되지만 인덱싱은 되지 않는다는 점이다. 인덱싱하려면 참조들을 담은 별도의 인덱스 배열을 따로 둬야 한다.
직렬화 없는 저장과 격리된 동시성
참조 모델도 흥미롭다. T&는 8바이트 머신 주소로 결코 댕글링되지 않으며, 같은 배열 안의 대상을 좁은 오프셋으로 가리키는 상대 참조(T&)도 있다. 링크가 자기 자신을 기준으로 한 상대값이기 때문에 구조체는 위치 독립적이 된다. 그 결과 저장은 바이트를 그대로 쓰는 일, 불러오기는 그 바이트를 그대로 읽는 일이 되어 별도의 직렬화기나 스키마, 포인터 보정이 필요 없다. from_bytes는 프레이밍과 모든 태그·길이·링크를 검사한 뒤에야 값으로 받아들이므로, 손상되거나 악의적인 파일은 와일드 참조가 아니라 false로 귀결된다.
동시성 모델은 공유 가변 메모리를 아예 두지 않는 쪽을 택했다. thread_fn과 그것이 호출하는 모든 코드는 자신만의 데이터 스택과 전역 변수 복사본을 가진 독립 프로그램으로 컴파일된다. 값은 타입마다 하나씩 있는 큐를 통해 건너가며, 어떤 깊이에서도 참조를 포함하지 않는 평탄한(flat) 값이어야 한다. 평탄한 구스 값은 연속된 메모리이므로 스레드 간 전달이 memcpy 한 번으로 끝나 저렴하다. thread_spawn_contained로 띄운 워커에서 검사 실패나 abort가 나면 그 워커만 종료되고, 메시지는 stderr가 아니라 기다리는 쪽으로 전달돼 감독 루틴이 로그를 남기고 재시작할 수 있다.
실무적 의미와 한계
구스는 모든 것을 모노모픽화(monomorphize)하고, 함수 값을 제네릭 파라미터로 전달해 클로저 객체나 함수 포인터 없이 모든 호출을 직접 호출·인라인 가능하게 만든다. 고차 함수가 겉모습 그대로의 루프로 컴파일된다는 뜻이다. 모든 인덱스는 경계 검사(bounds check)를 받되 컴파일러가 불필요하다고 증명한 검사는 제거하며, checkfree fn은 경계 검사가 하나라도 남으면 그 커널을 거부해 벡터화 가능한 코드를 강제할 수 있다. 외부 연동은 extern fn으로 C 심벌에 시그니처를 묶는 방식이고, 컴파일러는 프로그램 전체를 하나의 C 파일로 내보내며 내장된 TinyCC 백엔드로 그 C를 컴파일러 프로세스 안에서 바로 돌릴 수 있다. 윈도우(x64)·리눅스(x64)·macOS(애플 실리콘·인텔)용 사전 빌드 컴파일러가 릴리스 페이지에 그래픽·오디오·UI·물리 패키지와 함께 제공된다.
정리하면 구스가 내세우는 성능 이점의 상당 부분은 언어 철학이라기보다 메모리 모델에서 나온다. 힙 할당과 객체 단위 해제가 없고, 데이터가 연속적으로 놓이며, 링크가 좁고, 열거형이 담는 변형(variant) 크기에 맞춰진다는 점이다. 다만 벤치마크 수치가 해당 저장소 자체의 측정이라는 점, 가변 크기 원소 배열에서 인덱싱을 포기해야 하는 점, 스레드 간 값이 평탄해야 한다는 제약처럼 분명한 트레이드오프가 존재한다. 기존 코드베이스를 두고 당장 갈아탈 언어라기보다는, 할당 없는 연속 메모리 설계가 어디까지 성능과 안전을 동시에 끌어낼 수 있는지 보여 주는 실험으로 읽는 편이 적절하다. 저장소는 손실까지 포함한 전체 측정치를 results.md에, 무엇을 테스트했는지는 design.md에 공개하고 있으니 수치의 맥락은 직접 확인해 볼 만하다.