단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요?
아니면 가상 함수에 대한 호출 만? 그리고 가상 기능이 실제로 덮어 쓰여 졌는지 여부에 따라 속도가 영향을 받습니까, 아니면 가상 기능이있는 한 효과가 없습니다.
가상 함수를 사용하면 이러한 클래스의 객체를 다룰 때 하나 이상의 데이터 항목을 초기화, 복사해야하는 한 전체 클래스의 속도가 느려집니다. 6 명 정도의 구성원이있는 클래스의 경우 그 차이는 무시할 만합니다. 단일 char
멤버 만 포함 하거나 멤버가 전혀없는 클래스의 경우 차이가 눈에 띄게 나타날 수 있습니다.
그 외에도 가상 함수에 대한 모든 호출이 가상 함수 호출이 아니라는 점에 유의하는 것이 중요합니다. 알려진 유형의 객체가있는 경우 컴파일러는 정상적인 함수 호출을위한 코드를 내보낼 수 있으며, 그럴 경우 해당 함수를 인라인 할 수도 있습니다. 기본 클래스의 개체 또는 일부 파생 클래스의 개체를 가리킬 수있는 포인터 또는 참조를 통해 다형성 호출을 수행 할 때만 vtable 간접 지정이 필요하고 성능 측면에서 비용을 지불해야합니다.
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
하드웨어가 취해야하는 단계는 기능 덮어 쓰기 여부에 관계없이 본질적으로 동일합니다. vtable의 주소는 객체, 적절한 슬롯에서 검색된 함수 포인터 및 포인터가 호출하는 함수에서 읽습니다. 실제 성능 측면에서 분기 예측은 약간의 영향을 미칠 수 있습니다. 예를 들어, 대부분의 객체가 주어진 가상 함수의 동일한 구현을 참조하는 경우 분기 예측기가 포인터가 검색되기 전에 호출 할 함수를 올바르게 예측할 가능성이 있습니다. 그러나 어떤 함수가 일반적인 함수인지는 중요하지 않습니다. 대부분의 객체가 덮어 쓰지 않은 기본 케이스에 위임되거나 대부분의 객체가 동일한 하위 클래스에 속하므로 동일한 덮어 쓰기 케이스에 위임 될 수 있습니다.
깊은 수준에서 어떻게 구현됩니까?
모의 구현을 사용하여 이것을 시연하는 jheriko의 아이디어가 마음에 듭니다. 그러나 C를 사용하여 위의 코드와 유사한 것을 구현하여 낮은 수준을 더 쉽게 볼 수 있습니다.
부모 클래스 Foo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
파생 클래스 Bar
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
함수 f 가상 함수 호출 수행
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
보시다시피 vtable은 대부분 함수 포인터를 포함하는 메모리의 정적 블록입니다. 다형성 클래스의 모든 객체는 동적 유형에 해당하는 vtable을 가리 킵니다. 이는 또한 RTTI와 가상 함수 간의 연결을 더 명확하게합니다. 클래스가 가리키는 vtable을보고 클래스가 어떤 유형인지 확인할 수 있습니다. 위의 내용은 다중 상속과 같이 여러 방법으로 단순화되었지만 일반적인 개념은 건전합니다.
경우 arg
유형 인 Foo*
당신이 가지고 arg->vtable
있지만, 실제로 유형의 목적은 Bar
, 당신은 여전히의 올바른 주소를 얻을 vtable
. 그 이유 vtable
는는 호출 vtable
되거나 base.vtable
올바른 형식의 표현식에 관계없이 항상 개체 주소의 첫 번째 요소 이기 때문 입니다.
Inside the C++ Object Model
을 읽을 것을 제안하십시오Stanley B. Lippman
. (섹션 4.2, 페이지 124-131)