C # 및 Java 구현에서 객체에는 일반적으로 해당 클래스에 대한 단일 포인터가 있습니다. 단일 상속 언어이기 때문에 가능합니다. 그런 다음 클래스 구조에는 단일 상속 계층 구조에 대한 vtable이 포함됩니다. 그러나 인터페이스 메소드를 호출하면 다중 상속의 모든 문제가 있습니다. 이것은 일반적으로 구현 된 모든 인터페이스에 대한 추가 vtable을 클래스 구조에 넣어서 해결됩니다. 이를 통해 C ++의 일반적인 가상 상속 구현에 비해 공간이 절약되지만 인터페이스 메소드 디스패치가 더 복잡해져 캐싱으로 부분적으로 보상 할 수 있습니다.
예를 들어 OpenJDK JVM에서 각 클래스에는 구현 된 모든 인터페이스에 대한 vtable 배열이 포함됩니다 (인터페이스 vtable을 itable 이라고 함 ). 인터페이스 메소드가 호출되면이 배열에서 해당 인터페이스의 itable을 선형으로 검색 한 다음 해당 itable을 통해 메소드를 전달할 수 있습니다. 캐싱은 각 호출 사이트가 메소드 디스패치 결과를 기억하도록 사용되므로 구체적인 오브젝트 유형이 변경 될 때만이 검색을 반복해야합니다. 메소드 디스패치 용 의사 코드 :
// Dispatch SomeInterface.method
Method const* resolve_method(
Object const* instance, Klass const* interface, uint itable_slot) {
Klass const* klass = instance->klass;
for (Itable const* itable : klass->itables()) {
if (itable->klass() == interface)
return itable[itable_slot];
}
throw ...; // class does not implement required interface
}
OpenJDK HotSpot 인터프리터 또는 x86 컴파일러 의 실제 코드를 비교하십시오 .
C # (또는보다 정확하게는 CLR)은 관련 방법을 사용합니다. 그러나 여기서 이터 블은 메소드에 대한 포인터를 포함하지 않지만 슬롯 맵입니다. 클래스의 주요 vtable에있는 항목을 가리 킵니다. Java와 마찬가지로 올바른 itable을 검색하는 것은 최악의 시나리오 일 뿐이며 호출 사이트에서 캐싱하면이 검색을 거의 항상 피할 수 있습니다. CLR은 다른 캐싱 전략으로 JIT 컴파일 머신 코드를 패치하기 위해 Virtual Stub Dispatch라는 기술을 사용합니다. 의사 코드 :
Method const* resolve_method(
Object const* instance, Klass const* interface, uint interface_slot) {
Klass const* klass = instance->klass;
// Walk all base classes to find slot map
for (Klass const* base = klass; base != nullptr; base = base->base()) {
// I think the CLR actually uses hash tables instead of a linear search
for (SlotMap const* slot_map : base->slot_maps()) {
if (slot_map->klass() == interface) {
uint vtable_slot = slot_map[interface_slot];
return klass->vtable[vtable_slot];
}
}
}
throw ...; // class does not implement required interface
}
OpenJDK 의사 코드와의 주요 차이점은 OpenJDK에서 각 클래스에는 직접 또는 간접적으로 구현 된 모든 인터페이스의 배열이 있지만 CLR은 해당 클래스에서 직접 구현 된 인터페이스에 대한 슬롯 맵 배열 만 유지한다는 것입니다. 따라서 슬롯 맵을 찾을 때까지 상속 계층 구조를 위로 걸어야합니다. 깊은 상속 계층 구조의 경우 공간이 절약됩니다. 이것은 제네릭이 구현되는 방식으로 인해 CLR에서 특히 관련이 있습니다. 제네릭 특수화의 경우 클래스 구조가 복사되고 기본 vtable의 메소드가 특수화로 대체 될 수 있습니다. 슬롯 맵은 올바른 vtable 항목을 계속 가리 키므로 클래스의 모든 일반 특수화간에 공유 할 수 있습니다.
결말로 인터페이스 디스패치를 구현할 가능성이 더 많습니다. vtable / itable 포인터를 객체 또는 클래스 구조에 배치하는 대신 기본적으로 쌍인 객체에 대한 팻 포인터 를 사용할 수 있습니다 (Object*, VTable*)
. 단점은 포인터 크기를 두 배로 늘리고 콘크리트 유형에서 인터페이스 유형으로의 캐스트는 자유롭지 않다는 것입니다. 그러나 더 유연하고 간접 성이 적으며 인터페이스를 클래스 외부에서 구현할 수 있습니다. 관련 인터페이스는 Go 인터페이스, Rust 특성 및 Haskell 유형 클래스에서 사용됩니다.
참고 문헌 및 추가 자료 :
- 위키 백과 : 인라인 캐싱 . 값 비싼 분석법 조회를 피하는 데 사용할 수있는 캐싱 접근법에 대해 논의하십시오. 일반적으로 vtable 기반 디스패치에는 필요하지 않지만 위의 인터페이스 디스패치 전략과 같은 더 비싼 디스패치 메커니즘에는 매우 바람직합니다.
- OpenJDK Wiki (2013) : 인터페이스 호출 . itables를 토론하십시오.
- Pobar, Neward (2009) : SSCLI 2.0 내부. 이 책의 5 장에서는 슬롯 맵에 대해 자세히 설명합니다. 출판 된 적이 없지만 블로그에서 저자가 사용할 수있게되었습니다 . PDF 링크는 이후 이동했습니다. 이 책은 더 이상 CLR의 현재 상태를 반영하지 않습니다.
- CoreCLR (2006) : 가상 스텁 디스패치 . 에서 : 런타임의 책. 값 비싼 조회를 피하기 위해 슬롯 맵과 캐싱에 대해 논의하십시오.
- Kennedy, Syme (2001) : .NET 공용 언어 런타임의 제네릭 디자인 및 구현 . ( PDF 링크 ). 제네릭을 구현하기위한 다양한 접근 방식을 논의하십시오. 메서드는 특수화되어 vtable을 다시 작성해야하므로 제네릭은 메서드 디스패치와 상호 작용합니다.