# tsgo (typescript-go) Reference Analysis for Five > microsoft/typescript-go 프로젝트에서 배운 교훈과 Five에 적용한 내용 > > Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) > All rights reserved. --- ## 1. tsgo 프로젝트 개요 | 항목 | 내용 | |------|------| | 저장소 | https://github.com/microsoft/typescript-go | | 목적 | TypeScript 컴파일러를 Go로 재작성 | | 성과 | **10배 빠른 컴파일**, 메모리 2.9배 절감 | | 핵심 선택 | Go를 선택한 이유: 코드 구조가 1:1 매핑 가능, GC 제어력, 병렬화 용이 | --- ## 2. 핵심 교훈 7가지 ### 교훈 1: GC와 싸우지 마라, 설계로 맞춰라 ``` tsgo 접근: - 배치 컴파일: GC를 아예 실행하지 않아도 됨 (프로세스 종료 = 정리) - 서버 모드: AST는 장수명, "논리적 GC 시점"을 도메인 지식으로 판단 - Arena 할당자를 사용하지 않음 — 관용적 Go 코드로 충분 Five에 적용: ✅ ptrStore(글로벌 map+mutex) 제거 → Value.ptr(unsafe.Pointer) GC 직접 추적 ✅ 스칼라 타입은 힙 할당 없음 (Value struct에 인라인) → 향후: DBF 배치 스캔 시 GOGC=off 옵션 제공 가능 ``` ### 교훈 2: Value Semantics가 최대 승리 ``` tsgo 수치: - JS: 배열의 각 요소가 별도 힙 객체 (N+1 할당) - Go: 구조체 배열 = 단일 연속 할당 (1 할당) - boolean: JS 8+ bytes → Go 1 byte → 이것만으로 메모리 2.9배 절감의 주 원인 Five에 적용: ✅ Value = 24바이트 struct (Harbour HB_ITEM 32바이트 대비 25% 절감) ✅ []Value = 연속 메모리 (캐시 효율) ✅ 스칼라 값은 복사 전달 (포인터 추적 없음) → 1000개 Value 배열: 24KB (Harbour: 32KB) ``` ### 교훈 3: 핫 타입(30%)만 풀링하면 충분 ``` tsgo 접근: - Identifier 노드가 전체 AST의 ~30% 차지 - 256개씩 청크 할당하여 개별 힙 할당 대폭 감소 - 나머지 70%는 일반 Go 할당 사용 - 모든 것을 풀링하지 않음 — ROI가 없음 Five에 적용 (향후): → DBF 스캔 시 반복 생성되는 String Value를 sync.Pool로 풀링 → FOR/NEXT 루프의 임시 Value를 재사용 → 전체가 아닌 프로파일링으로 확인된 핫 경로만 최적화 ``` ### 교훈 4: 불변성으로 병렬화 ``` tsgo 접근: - 파싱 결과 AST = 불변 (immutable) - 4개 타입 체커가 같은 AST를 동시 읽기 (락 없음) - 각 체커는 자기만의 mutable 상태 보유 - Link Store: AST 수정 없이 노드별 메타데이터 부착 Five에 적용: ✅ Thread별 독립 Stack/Locals (goroutine-local, 락 없음) ✅ 심볼 테이블은 공유 읽기 전용 (sync.RWMutex) → 향후: 파싱된 AST 불변화, 여러 goroutine에서 병렬 코드 생성 → 향후: DBF 읽기 전용 모드에서 여러 goroutine 동시 스캔 (lock-free) ``` ### 교훈 5: UTF-8이 공짜 메모리 절감 ``` tsgo 수치: - JS UTF-16 → Go UTF-8: 문자열 메모리 50% 절감 - 서브스트링이 원본 메모리를 공유 (복사 없음) - 파서가 소스에서 식별자 추출 시 거의 할당 없음 Five에 적용: ✅ Go string = UTF-8 (Harbour의 바이트 문자열보다 유니코드 지원 우수) ✅ HbString.Data = Go string (immutable, 서브스트링 공유 가능) → 향후 파서: 소스 코드 서브스트링으로 토큰 추출 (할당 최소화) ``` ### 교훈 6: 구조적 유사성이 이론적 성능보다 중요 ``` tsgo 선택: - Rust/C++가 이론적으로 더 빠를 수 있었음 - 하지만 Go를 선택: TypeScript 코드와 1:1 구조 매핑이 가능 - 유지보수성 + 포팅 용이성 > 극한 최적화 - "최고의 메모리 관리 언어도 코드를 재작성해야 하면 쓸모없다" Five에 적용: ✅ gencc.c 패턴을 Go로 1:1 매핑 (hb_xvm* → Thread 메서드) ✅ Harbour RDD 가상 함수 테이블 → Go interface (구조 유사) ✅ Harbour HB_ITEM → Value (필드 의미 보존, 크기만 축소) → 기존 Harbour 소스를 읽으면서 Go 코드를 작성할 수 있음 ``` ### 교훈 7: 조기 off-heap 트릭을 피하라 ``` tsgo: - CockroachDB처럼 C.malloc으로 off-heap 이동하지 않음 - Go GC 범위 안에서 해결 - 작업 세트가 Go GC 모델에 잘 맞으므로 불필요 CockroachDB (대조): - 수십 GB 블록 캐시 → C.malloc으로 off-heap - GC 스캔 오버헤드가 심각한 경우에만 정당화 Five에 적용: ✅ unsafe.Pointer만 사용, C 할당/mmap은 사용하지 않음 → 향후: DBF mmap은 Go의 syscall.Mmap 사용 (GC와 무관한 파일 매핑) → 성능 병목이 확인되기 전까지 Go GC 범위 안에서 해결 ``` --- ## 3. tsgo 아키텍처 패턴과 Five 매핑 ### 3.1 노드 표현: Kind + Interface ``` tsgo: Node struct { Kind SyntaxKind, data nodeData(interface) } → Kind로 빠른 switch 디스패치 → data interface로 다형성 → 외부 구현 방지 (unexported method) Five: Value struct { scalar uint64, info uint64, ptr unsafe.Pointer } → info 상위 8비트로 빠른 타입 체크 → scalar로 스칼라 값 직접 접근 → ptr로 포인터 타입 접근 → 동일 원리: 판별자(discriminant) + 데이터 ``` ### 3.2 팩토리 패턴 ``` tsgo: NodeFactory가 모든 AST 노드 생성을 중앙화 → 풀링, 캐싱, 통계 수집의 단일 지점 → factory.NewIdentifier(), factory.NewBinaryExpression() Five (향후 적용): ValueFactory가 빈번한 Value 생성을 최적화 → MakeString(""): 빈 문자열 싱글턴 → MakeInt(0), MakeInt(1): 자주 쓰는 정수 캐싱 → sync.Pool for 임시 HbString 재사용 ``` ### 3.3 CheckerPool → ThreadPool ``` tsgo: - 4개 타입 체커를 병렬 실행 - 각 체커가 자기만의 캐시/상태 보유 - 불변 AST를 공유 읽기 - WorkGroup으로 작업 분배 Five (향후 적용): - goroutine pool로 병렬 DBF 스캔 - 각 goroutine이 자기만의 Thread 보유 - 불변 인덱스를 공유 읽기 (RWMutex) - channel로 결과 수집 ``` ### 3.4 Link Store → Thread-local State ``` tsgo: - AST를 수정하지 않고 노드별 메타데이터를 별도 저장 - 여러 체커가 같은 AST에 다른 메타데이터 부착 가능 Five: - WorkArea를 수정하지 않고 Thread별 커서 위치/필터를 별도 관리 - 여러 goroutine이 같은 DBF 파일에 다른 필터/커서 보유 ``` --- ## 4. Five Value 리팩터링 결과 ### 변경 전 (ptrStore 방식) ```go // Value = 16 bytes type Value struct { data uint64 // scalar OR uintptr (GC 불가!) info uint64 } // 글로벌 포인터 저장소 (GC와 싸우는 안티패턴) var ptrStore = &pointerStore{ items: make(map[uintptr]interface{}), // 메모리 누수! } func MakeString(s string) Value { hs := &HbString{Data: s} ptrStore.keep(hs) // 글로벌 mutex 잠금! return Value{ data: uint64(uintptr(unsafe.Pointer(hs))), // GC가 추적 불가 info: makeInfo(tString, 0, uint32(len(s))), } } ``` **문제:** - `ptrStore.mu.Lock()` — 모든 문자열/배열 생성 시 글로벌 mutex 경합 - `map[uintptr]interface{}` — 해제 시점 불명, 사실상 메모리 누수 - GC가 `uintptr`을 추적할 수 없어 조기 수거 위험 ### 변경 후 (tsgo 방식) ```go // Value = 24 bytes (Harbour 32B 대비 25% 절감) type Value struct { scalar uint64 // numeric/date/bool raw bits info uint64 // type tag + metadata ptr unsafe.Pointer // GC-traced pointer (nil for scalars) } func MakeString(s string) Value { hs := &HbString{Data: s} return Value{ info: makeInfo(tString, 0, uint32(len(s))), ptr: unsafe.Pointer(hs), // GC가 직접 추적! } } ``` **개선:** - 글로벌 mutex 제거 → 무잠금 (lock-free) - 메모리 누수 제거 → GC가 자연스럽게 수거 - `unsafe.Pointer` 필드는 Go GC가 직접 스캔 ### 벤치마크 비교 | 연산 | 16B+ptrStore | 24B+GC-safe | 차이 | |------|-------------|------------|------| | MakeInt | 4.9ns, 0 alloc | 5.0ns, 0 alloc | 동일 | | AddInt | 11.8ns, 0 alloc | 11.7ns, 0 alloc | 동일 | | TypeCheck | 0.11ns | 0.11ns | 동일 | 스칼라 연산은 성능 동일 (ptr 필드가 nil이므로 GC 스캔 비용 없음). --- ## 5. 추가 참조 프로젝트 ### esbuild (Evan Wallace) | 항목 | 내용 | |------|------| | 관련성 | Go 기반 번들러, tsgo에 영향을 줌 | | 핵심 패턴 | AST 전체를 3번만 순회 (캐시 지역성 최대화) | | Value semantics | boolean 1바이트, struct 임베딩으로 할당 최소화 | | 병렬화 | 파싱/코드 생성을 완전 병렬화 | | Five 적용 | 컴파일러 패스 수 최소화, 파싱과 코드 생성 병렬화 | ### CockroachDB / Pebble | 항목 | 내용 | |------|------| | 관련성 | Go 기반 대규모 데이터 처리 | | 핵심 패턴 | 블록 캐시를 C.malloc으로 off-heap 이동 | | 참조 카운트 | 캐시 값에 refcount 사용 (GC 대신) | | Five 적용 | DBF 페이지 캐시에 LRU + off-heap 검토 (향후, 필요 시) | ### Goja (JS engine in Go) | 항목 | 내용 | |------|------| | 관련성 | Go에서 동적 타입 언어 런타임 구현 | | 핵심 결정 | goroutine-safe 하지 않음 (단일 스레드 per runtime) | | Value 표현 | interface{} 사용 (boxing 비용 수용) | | Five 적용 | Thread별 독립 실행 (Goja와 동일), Value는 struct로 boxing 회피 | ### Go 1.23 unique 패키지 | 항목 | 내용 | |------|------| | 관련성 | 문자열 인터닝 (중복 제거) | | 핵심 | `unique.Make[string]()` → 동일 문자열은 같은 포인터 | | 내부 | concurrent hash-trie + weak pointer (GC 자동 정리) | | Five 적용 | 향후 심볼 이름, 필드 이름 인터닝에 활용 가능 (Go 1.23+) | ### Go Arena 제안 (issue #51317) | 항목 | 내용 | |------|------| | 관련성 | 동일 수명의 객체를 한 번에 할당/해제 | | 상태 | 실험적 (Go 1.20 arena 패키지, 이후 제거) | | tsgo 결정 | 사용하지 않음 — 관용적 Go로 충분 | | Five 적용 | DBF 배치 스캔의 임시 객체에 자체 Arena 패턴 적용 가능 (향후) | --- ## 6. 정리: Five가 tsgo에서 가져간 것 | tsgo 교훈 | Five 적용 | 상태 | |-----------|----------|------| | GC와 싸우지 마라 | ptrStore 제거, unsafe.Pointer 사용 | ✅ 완료 | | Value semantics | 24B Value struct, 스칼라 인라인 | ✅ 완료 | | 핫 타입 풀링 | sync.Pool for 빈번한 Value | ⬜ Phase 4 | | 불변성→병렬화 | Thread-local Stack/Locals | ✅ 완료 | | UTF-8 활용 | Go string 사용 | ✅ 완료 | | 구조 유사성 우선 | gencc.c → Thread 메서드 1:1 매핑 | ✅ 설계 완료 | | 조기 off-heap 금지 | Go GC 범위 안에서 해결 | ✅ 완료 | | Kind+Interface | Value.info(type tag) + Value.ptr | ✅ 완료 | | NodeFactory | ValueFactory (싱글턴 캐싱) | ⬜ Phase 4 | | CheckerPool | goroutine pool for DBF 스캔 | ⬜ Phase 5 | | Link Store | Thread-local WorkArea 상태 | ⬜ Phase 5 | --- ## 변경 이력 | 날짜 | 변경 내용 | |------|----------| | 2026-03-27 | 초기 작성. tsgo 분석, Value 리팩터링 (16B→24B), 7대 교훈 정리 |