Terms 24: Understand the virtual function, multi-inheritance, vain, and RTTI costs
This article contains some pictures, which cannot be attached to the document area, so I put the Word document into the zip file in the file exchange area, please download, please download
C compilers must implement each of the characteristics. The details of these implementations are of course determined by the compiler, and different compilers have different methods to realize the characteristics of the language. In most cases, you don't have to care about these things. However, some features have a great impact on the size of the object size and its member function, so there is a basic understanding of these features, knowing that the compiler may have done behind, it is very important. The most important example in this characteristic is a virtual function.
When a virtual function is called, the executed code must be consistent with the dynamic type of the object of the calling function; it is not important to point to the type of object or reference. How can the compiler provide this behavior efficiently? Most compilers are using Virtual Table and Virtual Table Pointers. Virtual Table and Virtual Table Pointers are typically called VTBL and VPTR, respectively.
A VTBL is usually a function pointer array. (Some compilers are used instead of arrays, but the basic method is the same), each class in the program declares that virtual functions or inherits the virtual function, it has its own VTBL, and the items in the class vtbl projects point to The virtual function implements the pointer. For example, this class definition:
Class C1 {
PUBLIC:
C1 ();
Virtual ~ c1 ();
Virtual void f1 ();
Virtual Int F2 (CHAR C) Const;
Virtual Void F3 (Const String & S);
Void f4 () const;
...
}
The Virtual Table array of C1 appears as shown below:
Note that the non-virtual function f4 is not in the table, and the constructor of C1 is not there. Non-virtual functions (including constructor, it is also defined as non-virtual functions) is implemented as normal C functions, so they have no special considerations in performance.
If there is a C2 class inheritance from C1, redefine some virtual functions it inherit, and add some of its own virtual functions,
Class C2: Public C1 {
PUBLIC:
C2 (); // non-virtual function
Virtual ~ c2 (); // Redefile function
Virtual Void F1 (); // Redefile Function
Virtual Void F5 (Char * STR); // New virtual function
...
}
Its Virtual Table project points to a function of the object. These items include pointers that point to C1 virtual functions that are not defined by C2:
This discussion introduces the first price required for virtual functions: You must leave a space for each Virtual Talbe that contains virtual functions. The size of the VTBL of the class is proportional to the number of virtual functions declared in the class (including virtual functions inherited from the base class). Each class should have only one Virtual Table, so the space required for Virtual Table will not be too large, but if you have a lot of classes or have a lot of virtual functions in each class, you will find that VTBL will take a lot of address space. .
Because each class is required to copy only one VTBL copy, the compiler will definitely encounter a tricky question: Where is it? Most programs and libraries are connected by multiple Object (target) files, but each Object file is independent. Which Object file should include a given VTBL? You may think that it is placed in the object file containing the main function, but the library has no main, and no matter how the source file containing Main does not involve a lot of classes that need VTBL. How do the compiler know that they are asked to build that VTBL? A different approach must be taken, and the compiler vendor is divided into two camps. For vendors that provide integrated development environments (including compilers and connections), a simple way is to generate a VTBL copy for each Object file that may require VTBL. The connection program then removes the repeated copy, reserves an instance for each VTBL in the final executable or library.
More common design methods are to use the heuristic algorithm to determine which Object file should contain the type of VTBL. Usually the heuristic algorithm is like this: To generate a class of VTBL in an Object file, the Object file contains the first non-inline, non-pure virtual function (non-INLINE NON-PURE VIRUAL FUNCTION) defined by this class. (That is, a class realization). Therefore, the VTBL of the above C1 class will be placed in an Object file containing C1 :: ~ C1 (not inline), and the VTBL of the C2 class is placed in an Object file contained in C1 :: ~ c2 (not inner) Function).
In practice, this heuristic algorithm has a good effect, but if you are too much to declare the virtual function as the inline function (see Effective C Terms 33). If all the virtual functions in the class declares that the inline function is declared, the heuristic algorithm fails, and most of the heuristic algorithm-based compilers generate a class of VTBLs in each Object file using it. In large systems, this will cause the program to include hundreds of VTBL copies of the same class! Most compilers that follow this heuristic algorithm will give you some ways to manually control the generation of VTBL, but a better way to solve this problem is to avoid declaring the virtual function as an inline function. Here we will see that there are some reasons that lead to the current compiler generally ignore the Inline directive of the virtual function.
Virtual Table only features half a mechanism for virtual functions, if only these are useless. Only when using some way to point out VTBLs corresponding to each object, they can use it. This is the work of the Virtual Table Pointer, which is to build this connection.
Every object that declares the virtual function has it, it is a visible data member, pointing to the Virtual Table of the corresponding class. This invisible data member is also called VPTR, and the compiler is added in the object, and only the compiler is known. In theory, we can think that the layout of objects containing virtual functions is this:
This picture indicates that VPTR is located at the bottom of the object, but do not be deceived, the location of different compilers places it is different. In the case where inheritance is present, the VPTR of an object is often surrounded by the data member. If there is multiple inheritance, this picture will become more complicated, and we will discuss it. Now simply remember the second price required for virtual functions is: In the object that contains virtual functions, you must pay for additional pointers.
If the object is small, this is a big price. For example, if your object is only 4-bit member data, then additional VPTR will double the member data size (assuming the VPTR size is 4 bits). In the system that is limited, this means you have to reduce the number of builds. Even in the system where memory is unlimited, you will also find that this will reduce the performance of the software, because larger objects may not be suitable for placing a cache or virtual memory page (Virtual Memory Page), which may make The system change page operation is increased. If we have a program, there are several C1 and C2 objects. Objects, VPTR and the relationship between VTBL we talked about, we can imagine this in the program:
Consider this program code:
Void makeacall (C1 * PC1)
{
PC1-> F1 ();
}
Call the virtual function F1 by the pointer PC1. Just watch this code, you won't know that it is called that the F1 function - C1:: F1 or C2 :: F1, because PC1 can point to the C1 object or point to C2 objects. Although such a compiler still has to generate a code for a call to the MakeAcall's F1 function, it must ensure that the function call must be correct regardless of the object of PC1 points. The code generated by the compiler will do this:
1. Find the VTBL of the class through the VPTR of the object. This is a simple operation because the compiler knows where to find VPTR in the object (after all, they are placed by the compiler). So this price is just an offset adjustment (to obtain VPTR) and indirect addressing of a pointer (to obtain VTBL).
2. Find the pointer to the called function within VTBL (in the previous example is F1). This is also very simple, because the compiler has assigned a unique index to each virtual function in VTBL. The price of this step is just an offset in the VTBL array.
3. Call the function points to the function points to the pointer found by the second step.
If we assume that each object has a hidden data called VPTR, and the index of F1 in VTBL is i, this statement
PC1-> F1 ();
The resulting code is like this
(* PC1-> VPTR [I]) (PC1); // Calling the i-th unit referring to VTBL
// The function of the PC1-> VPTR
// Point to VTBL; PC1 is made
// This pointer is passed to the function.
This is almost the same as the efficiency of calling non-virtual functions. In most computers, it has implemented very few instructions. The cost of calling the virtual function is basically the same as the function of the function pointer. The virtual function itself is usually not a bottleneck of performance.
In actual operation, the cost of virtual functions is related to the inline function. In fact, the virtual function cannot be inline. This is because "Inline" means "instead of instead of function calls in compilation", "But" virtual "" virtual "refers to" until running when it is runtime. Which function is to be called? "If the compiler is called in a function of a function, you can know why it does not call the function inline. This is the third price required for virtual functions: You actually give up the use of inline functions. (When the virtual function is called by the object, it can be inline, but most virtual functions are called by the object's pointer or the reference is called, because this call is a standard call mode, Therefore, the virtual function can actually be inline.)
So far we discussed so far applies to single success and more inherits, but more inheritance introduces, things will become more complex (see Effective C Terms 43). This details is discussed in detail, but in multiple inheritance, the offset calculation to find VPTR in the object will become more complicated. There are multiple VPTRs in a single object (one base class corresponding to one); except that the individual VTBL we have discussed, special VTBL is generated for the base class. Therefore, there is an increase in space for each class and the amount of virtual functions in each object, and the cost required to run when the runtime also increases. Multi-inheritance often leads to demand for virtual basis. No virtual base class, if a derived class has more than one inheritance path from the base class, the data member of the base class is copied into each inherited class, and each path between the inheritance class and the base class has a copy. Programmers generally do not want to happen, and the base class is defined as a virtual base class to eliminate this copy. However, the virtual base class itself will cause their own expense, because the realization of the virtual base class often uses a pointer to the virtual base class as a means to avoid copying, one or more pointers are stored in the object.
For example, consider the following picture, I often call it "The Dreaded Multiple Inheritance Diamond"
Here A is a virtual base class because B and C virtual inherits it. Use some compiler (especially the old compiler), the D object will produce this layout:
Put the base class's data member in the bottom of the object, which is somewhat surprises, but it often does this. Of course, how to achieve the freedom of the compiler, how do they want to do, this picture is just a conceptual description of the virtual base class to cause an additional pointer, so you should not use this picture outside this range. Some compilers may add less pointers, and some compilers use some way to do not add additional pointers at all (this compiler makes VPTR and VTBL burden double responsibility).
If we commented this picture with the previous picture to get the picture of the Virtual Table Pointer to the object, we will recognize that if there is any virtual function in the base class A in the above inheritance system, the memory layout of the object D is like this. of:
Here, the part is added by the compiler, I have made shadow processing. This picture may be misleading because the area ratio between the shaded portion and the non-shaded portion is determined by the amount of data in the class. For small classes, the extra price is large. For classes containing more data, the additional cost is not large, although it is also worth noting.
There is still a strange thing that although there are four classes, the above chart has only three VPTR. As long as the compiler likes, of course, four VPTR can be generated, but three already enough (it finds that B and D can share a VPTR), most compilers will use this opportunity to reduce the additional burden generated by the compiler.
We have now seen that the virtual function makes the object more, and cannot use inline, we have tested excessive inheritance and virtual base classes will also increase the size of the object. Let us turn to the last topic, runtime type identification (RTTI).
RTTI allows us to find information about objects and classes at runtime, so there is definitely some place to store this information, let us query. This information is stored in the type of type Type_info, you can access a class Type_info object by using the TypeID operator.
Only one RTTI copy is required in each class, but there must be a way to get any object information. In fact, this is not very accurate. Language specification This description: We guarantee that you can get an object dynamic type information, if this type has at least one virtual function. This makes the RTTI data seems to be some like Virtual Function Talbe (virtual functions). Every class we only need a copy of information, and we need a method to get appropriate information from any object containing virtual functions. The similarities between this RTTI and Virtual Function Table are not coincidental: RTTI is designed to be implemented on the basis of the class's VTBL. For example, the index 0 of the VTBL array may contain a pointer to a Type_info object, which belongs to the CTBL class. The VTBL of the above C1 is like this:
Using this implementation method, the RTTI spented space is the space that stores the Type_info object in each of the occupied additional units in each class. Just like the memory space occupied by Virtual Table in most programs is not worth noting, you are unlikely to encounter problems because of the Type_info object size.
The following is a summary of the main cost required for virtual functions, multi-inheritance, false base classes, and RTTI:
FEATURE
Increase of Objects
Increasesper-Class Data
Reducesinlining
Virtual functions
YES
YES
YES
Multiple Inheritance
YES
YES
NO
Virtual base classes
Often
Sometimes
NO
RTTI
NO
YES
NO
Some people will be very surprised after seeing this form, they announce the "I should use C". well. But remember that if there is no functionality provided by these features, you must be manually coded. In most cases, your artificial simulation may be lower than the code efficiency generated by the compiler and poor stability. For example, using a nested Switch statement or a stacked IF-THEN-ELSE statement analog virtual function call, the code generated is more than the virtual function call, and the code running speed is also slower. Moreover, you must manually track object types, which means that objects will bring their own type tags (TYPE TAG). So you won't get smaller objects.
Understand the virtual function, multiple inheritance, vain class, and RTTI's cost is important, but if you need these features, no matter what kind of method you have to pay for this, understand this is equally important. Sometimes you do have some reasonable reason to bypass the service generated by the compiler. For example, hidden VPTR and pointers that point to virtual base classes make it difficult to store C objects or cross-process in the database, so you may want to simulate these features in a way, which can be easier to complete these tasks easier. However, from the point of view of efficiency, you should write the code better than the code generated by the compiler.