동적 객체 속성 조회를 빠르게 하는 shape와 인라인 캐시
Max Bernstein의 글은 동적 객체의 속성 이름과 슬롯 위치를 shape로 연결하고, 인라인 캐시가 조회 지점에서 shape와 슬롯을 재사용하는 원리를 설명합니다. Python 축약 구현과 C++ 메모리 배치 스케치로 작동 방식을 보여 주며, 속성 추가 순서에 따라 shape가 달라지고 다른 클래스의 객체도 같은 shape를 공유할 수 있다고 설명합니다.
동적 언어에서 obj.x 같은 속성 접근은 속성 이름으로 객체 안의 값을 읽거나 바꾸는 작업입니다. Max Bernstein은 2026년 10월 21일 블로그 글에서 이 접근을 shape와 인라인 캐시로 최적화하는 원리를 작은 구현으로 설명합니다. 예제는 CF Bolz-Tereick의 《A Simple Object Model》에서 인스턴스 속성과 map 부분을 추려 재구성했고, 뒤에는 메모리 배치의 기본 모양을 보여 주는 작은 C++ 런타임 스케치도 덧붙였습니다.
이름 조회를 저장 위치 조회로 바꿉니다
단순한 객체 모델에서는 객체가 속성 이름과 값을 별도 해시 테이블에 저장합니다. obj.a를 읽을 때 런타임은 이름 a를 해시해 테이블을 찾고 값을 꺼냅니다. 해시 테이블 조회가 O(1)로 분류되더라도 해시 계산과 탐색이 필요하고, 객체에서 별도 테이블로 이동하는 의존 메모리 접근도 더해집니다. 속성 접근이 잦은 코드에서는 이 간접 참조가 반복 비용으로 이어집니다.
shape 또는 map은 객체의 배치를 나타내는 별도 정보입니다. 속성 이름과 값 자체가 아니라 값이 놓인 슬롯 번호를 연결해 둡니다. 예제에서 shape는 a가 0번, b가 1번 슬롯에 있다고 기록하고, 객체는 실제 값들을 순서가 있는 storage에 보관합니다. 런타임은 map에서 속성 이름에 해당하는 슬롯을 찾은 뒤 그 위치의 값을 읽습니다. Bernstein의 Python 예제에서는 Python이 객체 메모리 배치를 직접 제어하지 못해 storage가 별도 배열로 남습니다. C++이나 Rust로 구현하는 런타임에서는 속성 값을 객체 헤더와 함께 메모리에 둘 수 있다고 설명합니다.
속성 값을 바꾸는 일과 새 속성을 더하는 일은 다르게 처리합니다. 이미 있는 a에 새 값을 넣으면 shape가 가리키는 슬롯의 값만 바뀝니다. 새 속성 c를 추가하면 런타임은 기존 shape에서 c가 추가된 새 shape로 전이하고, 새 값을 다음 슬롯에 둡니다. 이 예제의 map은 불변이며, 빈 shape에서 속성을 추가하는 경로를 트리처럼 이어 갑니다. 같은 속성 추가 경로를 가진 객체는 해당 shape를 공유할 수 있습니다.
인라인 캐시가 슬롯 번호를 재사용합니다
shape만으로는 일반적인 obj.x 호출에서도 런타임이 이름 x를 map에서 찾아 슬롯 번호를 얻는 과정이 남습니다. 인라인 캐시는 속성 조회 코드 위치별로 이 정보를 기억합니다. 프로그램의 해당 지점에서 속성 이름은 대체로 고정되므로, 캐시는 처음 이름을 조회한 뒤 기대하는 shape와 슬롯 번호를 저장합니다.
다음 조회에서 객체의 shape가 캐시에 저장된 shape와 같으면 런타임은 이름을 다시 찾지 않고 storage의 해당 슬롯을 읽습니다. shape가 다르면 map에서 이름의 위치를 다시 찾고, 속성이 있으면 새 shape와 슬롯 번호로 캐시를 갱신합니다. 값이 바뀌어도 shape가 그대로라면 기존 슬롯 정보가 유효합니다. 새 속성이 추가돼 shape가 바뀌면 캐시가 맞지 않아 이름 조회 경로를 거칩니다. Bernstein의 축약 구현은 없는 속성 조회 결과를 캐시에 따로 저장하지 않습니다.
캐시는 객체 하나에만 묶이지 않습니다. 같은 속성 배치를 가진 여러 객체가 같은 shape를 공유하면 같은 조회 지점의 캐시를 함께 활용할 수 있습니다. 예를 들어 여러 Point 객체에서 x를 읽는 코드가 같은 shape를 만나면 한 번 저장한 슬롯 번호를 재사용합니다. 캐시는 shape와 슬롯을 저장할 공간을 쓰는 대신 반복되는 이름 탐색을 줄입니다.
shape는 속성의 이름과 배치를 나타내며 클래스 자체를 식별하지는 않습니다. 서로 다른 클래스의 객체도 x와 y를 같은 순서로 추가하면 같은 shape를 공유할 수 있습니다. 그러면 한 조회 지점의 캐시가 여러 종류의 객체에서 적중할 수 있습니다. 반면 Ruby의 attr_reader처럼 클래스에 따라 메서드를 정한 뒤 인스턴스 속성을 찾는 경우에는 클래스에 따른 메서드 조회와 shape에 따른 속성 조회를 각각 거칩니다.
속성 추가 순서는 shape에 영향을 줍니다. a 다음 b를 추가한 객체와 b 다음 a를 추가한 객체는 같은 이름을 가졌어도 서로 다른 shape를 얻습니다. 초기화 때 조건에 따라 속성을 넣는 코드도 객체마다 배치가 달라지는 원인이 됩니다. 이런 객체들이 같은 속성 조회 지점에 섞이면 캐시가 기대하는 shape와 다른 객체를 만나는 일이 잦아집니다. 따라서 속성 이름이 고정되고 객체의 배치가 반복되는 코드에서 shape와 인라인 캐시의 빠른 경로가 단순하게 작동합니다.
Bernstein은 Python의 축약 구현으로 이름 조회와 슬롯 접근의 관계를 보여 준 뒤, C++로 객체 헤더에 shape를 두고 속성을 배치하는 작은 런타임 스케치를 제시합니다. 이 스케치는 객체를 할당하고 속성을 더하는 예제로, 실제 VM의 메모리 배치가 어떤 모습인지 설명합니다. 글의 초점은 동적 속성의 유연성을 유지하면서 반복 접근에서는 shape를 확인하고 슬롯을 읽는 경로를 쓰는 원리입니다.