Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion docs/design/typedpython.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ GC 추적), refcount 증감, 해제(`subtype_dealloc`)를 그대로 치르기
하나를 고른 것으로 본다. 컴파일된 코드는 GIL 을 놓지 않으므로, 컴파일 구간 안에서 다른 스레드가 끼어드는
실행은 생기지 않는다. 이는 CPython 이 그 구간에서 전환하지 않은 실행과 같다.

> **free-threaded 3.15t 에서 다시 볼 점 (#193, #159).** 위 문장은 GIL 빌드의 논증이다. 3.15t 에는 GIL 이 없어 다른
> 스레드가 컴파일 구간과 동시에 돈다. 그래서 다른 스레드에서 보일 수 있는 객체에 대한 아래의 최적화(특히 나의
> refcount 생략)는 스레드 지역으로 증명된 객체로 제한한다. 가상 객체는 정의상 실체화 전까지 다른 스레드가 볼 수
> 없으므로 영향이 없다.

#### 가. 가상 객체 (부분 탈출 분석 + 지연 실체화)

PyPy·GraalVM 과 같은 방식이다. N-11 클래스의 객체를 PyObject 대신 C 구조체(필드 값만)로 두고, 관측
Expand Down Expand Up @@ -254,7 +259,9 @@ PyPy·GraalVM 과 같은 방식이다. N-11 클래스의 객체를 PyObject 대
놓는다. 바깥 객체가 소유되어 있고, 그 슬롯이 그동안 쓰이지 않고, 사용자 코드가 돌지 않으면 빌린 참조로
넘길 수 있다.
- `sys.getrefcount` 는 호출로만 관측되고, 호출은 위 조건으로 막혀 있으므로 관측되는 refcount 는 늘 정확하다.
다른 스레드는 GIL 때문에 그 사이를 보지 못한다. free-threaded 빌드는 범위 밖이다(이 모듈은 GIL 을 다시 켠다).
**free-threaded 3.15t 에서는 이 논증이 성립하지 않는다(#193, 결정 D5).** GIL 이 없으므로 다른 스레드가 그
사이에 슬롯을 쓰고 자식을 해제할 수 있다. 그래서 빌린 읽기와 refcount 생략은 **스레드 지역으로 증명된 객체에만**
적용하고, 나머지는 실제 참조를 잡거나 CPython 의 객체별 critical section 안에서 읽는다 (§4.3.5).

#### 다. 영역(arena) 할당

Expand Down Expand Up @@ -308,6 +315,73 @@ PyPy·GraalVM 과 같은 방식이다. N-11 클래스의 객체를 PyObject 대

이 셋은 N-12 이전부터 있는 경계도 포함한다. 1번이 결정되어 N-12 구현을 시작한다. 2번은 #141(1단계 완료), 3번은 #142.

### 4.3.5 소유권 추론, 메모리, 인터프리터 최소 상주 (설계, 결정 2026-10-05)

사용자 질문 (2026-10-05): "GC의 동작을 smart pointer로 전환하는 컴파일러를 개발하는게 가능할까?", 그리고
CPython 의 두 약점(Rust 대비 연산 속도와 메모리 사용) 중 메모리가 지금 설계로 얼마나 풀리는가. 근거와 측정은
[`typedpython-ownership-memory.md`](../investigations/typedpython-ownership-memory.md).

**전제.** CPython 객체는 이미 객체 안에 refcount 를 둔 smart pointer 이고, GC 는 순환만 맡는다. 그래서 "smart
pointer 로 전환" 의 실제 뜻은 컴파일 타임에 소유권을 추론해 refcount 연산과 GC 부담을 없애는 것이다. GC 를
통째로 없애지는 못한다. 인터프리터와 서드파티 코드가 같은 힙을 쓰고, 사용자가 순환 구조를 만들 수 있다.

#### 가. 소유권 추론 (관측 차이 없음, 결정 불필요)

§4.3.4 의 가상 객체(고유 소유), refcount 생략(빌림), 영역 할당(영역 소유)을 함수 경계 너머로 넓힌다.

- 컴파일된 함수의 매개변수와 반환값에 **소유 이전 / 빌림** 을 추론해 붙인다. 선례는 Lean 4 의 borrow 추론,
Koka Perceus, Lobster(런타임 refcount 연산 약 95% 제거 보고), Swift OSSA 다. 이들은 불변성이나 단일 소유자
가정에서 이득을 얻는데, 파이썬에는 그 가정이 없으므로 이득은 증명되는 범위에 한정된다.
- **free-threaded 에서 효과가 크다.** 3.15t 에서 refcount 증감 한 쌍은 현재 스레드 소유 객체에 약 3.2 ns, 다른
스레드가 만든 객체에 약 14.2 ns 다(실측). 다만 빌린 참조는 **스레드 지역으로 증명된 객체에만** 쓴다(#193).
- Perceus 식 제자리 재사용(refcount 1 이면 해제 대신 재사용)은 `gc.get_objects` 순서와 `tracemalloc` 에서
관측되고, 유일 참조 판정 API 가 stable ABI 밖이라 지금은 하지 않는다.

#### 나. 메모리: 무엇이 줄고 무엇이 남는가

실측(Rust 대비, 3.15t): float 40 B 대 8 B, int 44 B 대 8 B, 실수 필드 2 개 객체 약 157 B 대 16 B, 정수 리스트
원소당 56 B 대 8 B, 빈 인터프리터 실점유 약 7.6 MB 대 Rust 빈 프로그램 RSS 1.4 MB. 지금 설계(가상 객체, 영역,
i64/f64 지역)는 **컴파일된 구간 안의 임시 값**만 줄인다. 컨테이너·전역에 사는 오래 가는 데이터와 인터프리터
바닥은 그대로다.

결정에 따라 다음을 한다.

- **스칼라 슬롯 직접 저장 (D1, D2, D3).** `@compiled` 클래스에서 `int`·`float` 로 선언된 슬롯은 객체 포인터
대신 i64/f64 값을 객체 안에 저장한다(객체당 약 3.3 배 감소, 실측 근거는 조사 문서). 인터프리터가 읽을 때마다
새 int/float 를 만든다.
- **D1 (예):** 슬롯에서 읽은 정확한 `int`·`float` 값의 동일성(`is`, `id`)은 동일성 보장에서 뺀다. 언어 규격이
불변 값의 동일성을 구현에 맡기므로 "어떤 유효한 구현과 구별할 수 없다" 는 원칙 안이다. 서브클래스 값은 대상이
아니다.
- **D2 (예, 조건부):** 이 클래스의 타입은 컴파일러가 `PyType_FromSpec` 으로 만든다. `__module__`,
`__qualname__`, `__slots__`, MRO, pickle, `inspect` 결과가 원래 `class` 문과 같음을 차등 시험으로 보인다.
- **D3 (예, 조건부):** 이 클래스의 슬롯에 넣는 값은 선언 타입을 강제한다(그 자리에서 `TypeError`). 사용자
코드는 이미 게이트를 통과하므로 막히는 것은 타입을 어긴 외부 동적 코드다. opt-in 이 곧 동의다.
- **GC 없는 클래스 (D4, 아니오).** GC 헤더 제거는 GIL 빌드에서 객체당 16 B 를 줄이지만 3.15t 에서는 0 B 다(실측).
유일한 대상이 3.15t 이므로 하지 않고, 그래서 상속 금지(final)도 두지 않는다.
- **타입 확정 컨테이너.** 압축 배열은 `array.array` 나 별도 opt-in 타입으로 안내한다. `list` 의 표현은 바꾸지
않는다(동일성 위험이 가장 크다).
- **기대치.** 타입을 확정한 데이터는 Rust 의 약 1~2 배, `dict`·`str`·일반 클래스 위주의 코드는 지금 수준(3~10 배)
이 크게 줄지 않는다. 이 문단은 추정이고, 벤치마크에 실점유 메모리 측정을 더해 기준선으로 박는다.

#### 다. 인터프리터 최소 상주

인터프리터는 갱신 가능한 코드(FastTrack), 서드파티 동적 코드, 개발 중 핫 리로드를 맡고, 그 밖에서는 바닥만
쓴다. 바닥(실점유 GIL 5.7 MB, 3.15t 7.6 MB)은 CPython 런타임이 정하고, 앱이 정하는 것은 import 다(흔한 모듈 다섯
개에 10~12 MB).

- **PEP 810 `lazy import` (결정 불필요).** 3.15 에 들어갔다. 쓰지 않는 import 다섯 개를 lazy 로 두면 3.15t RSS 가
30.3 MB 에서 17.5 MB 로 줄었다(실측). 컴파일러가 몰래 넣지 않는다(import 시점과 부작용이 바뀐다). pythonx
라이브러리와 생성 코드에서 적극적으로 쓴다.
- **D6 (예, 앱 배포 빌드만):** `site` 를 끈다(약 1 MB). pypackpack 이 `sys.path` 를 정하므로 할 일이 없다.
`.pth` 처리와 `quit`/`exit` 이 사라지는 점을 문서화하고, 개발 모드는 켜 둔다.
- **D7 (예):** free-threaded 빌드는 해제한 메모리를 OS 에 돌려주지 않았다(객체 100 만 개 해제 뒤 점유 그대로,
실측). 우리가 직접 빌드하는 FT CPython 에 플랫폼 메모리 압박(Android `onTrimMemory`, iOS 메모리 경고) 때
mimalloc 정리를 부르는 최소 패치를 넣고 upstream 에도 제안한다.
- **D8 (지금은 아니오):** 클래스·함수 정의를 C 초기화 코드로 바꿔 인터프리터 의존을 줄이는 설계는 규모와 위험이
크고, 같은 문제를 lazy import 가 더 싸게 줄인다.
- 효과가 작아 하지 않는 것: `-OO`(0.35~0.8 MB, `assert` 도 사라짐), deopt 원본 지연 로딩(함수 1000 개당 약
0.4 MB), 동결 모듈 끄기(오히려 4.4 MB 증가).

### 4.4 Kotlin 경계의 직접 호출

이 플랫폼에서만 가능한 이점이다.
Expand Down Expand Up @@ -568,6 +642,8 @@ upstream 대비 수치 오차와 커널 시간을 기준선으로 박아 둔다.
에러 수와 그 원인을 기준선으로 박아 둔다.
- **서드파티 스텁 부재.** 스텁이 없는 의존성을 많이 쓰면 `Any` 출처가 늘어 에러가 잦아진다. 이 경우를
에러로 둘지, 경계 한 곳의 명시적 캐스트로 해소하게 할지가 채택을 가른다.
- **메모리 기대치.** 타입을 확정한 데이터만 Rust 근처로 간다. `dict`·`str`·일반 클래스 위주의 코드와 인터프리터
바닥은 크게 줄지 않는다 (§4.3.5).
- **파이썬 의미론과의 괴리.** 금지 목록이 실제 코드에서 얼마나 흔한지 측정해야 한다. 너무 흔하면
`@compiled` 가 채택되지 않는다.

Expand Down Expand Up @@ -595,3 +671,5 @@ upstream 대비 수치 오차와 커널 시간을 기준선으로 박아 둔다.
8. **§5.10 의 "판단 대기" 목록.** 융합, 자동 미분, `vmap`, 변환 합성, 샤딩, TPU · 최신 GPU 성숙도, 내보낸
산출물의 호환 보장 각각을 "torch 에 맡김" (torch 가 JAX 만큼 성능을 낸다는 근거가 있을 때만),
"우리가 만든다", "범위 밖" 중 하나로 정한다.
9. ~~메모리와 인터프리터 바닥 (D1~D8)~~, **결정됨 (2026-10-05, 메인테이너):** D1 예, D2 예(조건부), D3 예(조건부),
D4 아니오, D5 결함 수정(#193), D6 예(앱 배포 빌드만), D7 예, D8 지금은 아니오. 내용은 §4.3.5.
Loading
Loading