Outline
Other Material
Endpoints are clear, but there are lots of gray areas in the middle.
Java JIT systems are one of those gray areas between compilers and interpreters
A method is eventually invoked just like any other function. This means that the receiver just be passed as an argument. Traditionally, it is passed as the first argument.
This means a method call, such as
aCardPile->addCard (currentCard)
Is translated into
addCard (aCardPile, currentCard)
(This is ignoring the method lookup process, which we will discuss shortly).
On the other side, the receiver pseudo-variable is just a formal argument:
Instead of
void CardPile::addCard (Card * aCard) { ... }
We have
void addCard (CardPile * this, Card * aCard) { ... }
The first argument can then be used to access data members and other methods.
In languages that permit both virtual and non-virtual methods (such as C++) a non-virtual method is translated into a simple procedure call.
An encoded name is sometimes called a mangled name. You will sometimes see mangled names in error messages generated by a linker.
Now consider those methods defined in a parent class, but used by a child class.
How is it that this mechanism can work? Normally you cannot change the types of arguments (recall that the receiver is just an argument).
Solution is that the data associated with an instance of a child class is an extension of the data associated with the parent class.
This means that data fields in the parent class will be found at the same offset in the child class.
The idea that a child is an extension of the parent explains one of the most vexing problems in the implementation of multiple inheritance.
A child can extend one parent, or the other, but not both.
That is the offset of data fields in the child cannot simultaneously match both parents.
The idea that a child can extend the data area of the parent also makes it difficult to support both the following goals
Most OO languages uphold (2) and abandon (1), C++ is an exception in that it upholds (1) and therefore abandons (2).
We next consider those methods that are defined in a parent class, and overridden in a child class.
Problem, how can a polymorphic method invocation find the right method? Note that the right method can change during the course of execution, even for the same method call.
CardPile * aCardPile = new DiscardPile(); Card * aCard = ... ; aCardPile->addCard (aCard); // how to find the right method
The solution is that every object contains an extra hidden data field. This data field points to an array of pointers to functions. The array is determined by the current dynamic type, and is shared by all instances of the class.
The offset of each method can be determined at compile time.
Two instances of a class will share the same virtual method table.
Each object maintains a pointer to a table, called the virtual method table.
Virtual methods are identified by a fixed address in this table.
A method call, such as
A.foo(B, C)
is translated into
(* A.virTable[idx])(A, B, C)
When a subclass is created, a new virtual method table is generated.
Even though the overhead of a virtual call is small, it can still add up.
If the (dynamic) class of the receiver is know, a virtual call can simply become an ordinary procedure call
Good optimizing compiles spend a considerable amount of time tracing possible execution flows to gather this information.
Sometimes methods can even be expanded in-line at the point of call.
In languages without static typing it is not practical to use a virtual table, since such a table would need to encode all methods, not simply those in a given class hierarchy.
An alternative technique uses a pointer to a list of selector/method pairs.
When a method is invoked, a run-time search is performed to match the method being called with the list of known selectors, until an appropriate method is found.
In Objective-C the messages
[ neighbor checkrow: row column: column ]
is translated into
objc_msgSend(neighbor, "checkrow:column:", row, column)
An important difference is that the dispatch table is searched at run-time, not at compile time.
Objective-C uses a linear list for the table, Smalltalk generally uses a balanced search tree, but the idea is similar.
In order to avoid the cost of a dynamic search of dispatch tables, a single global cache can be used to hold frequently invoked methods.
The cache is used as a large hash table.
Prior to searching a dispatch table, the a single hashed entry is examined - if it matches the selector being sought, the method is used, if not the dispatch table is searched and the new entry replaces the value in the hash table.
Assuming a good hash function is used, efficiency can be very high.
An implementation technique that is widely used (Smalltalk, Java)
A currently popular mix between compilers and interpreters.
Key idea, first time a method is executed, translate the bytecode into native machine code.
Gives fast execution time, pay penalty for translation (and analysis if you want to do a good job).
Currently very popular with Java systems.