Supporting dynamic languages on the Java virtual machine

dvanhorn <[email protected]>
Newsgroups gmane.org.ballistichelmet.lambda
Message-ID <[email protected]>
Supporting dynamic languages on the Java virtual machine
Olin Shivers
MIT Artificial Intelligence Laboratory
April 25, 1996
Abstract

In this note, I propose two extensions to the Java virtual machine (or VM) to 
allow dynamic languages such as Dylan, Scheme and Smalltalk to be efficiently 
implemented on the VM. These extensions do not affect the performance of pure 
Java programs on the machine. The first extension allows for efficient 
encoding of dynamic data; the second allows for efficient encoding of 
language-specific computational elements.
Introduction

Java is easily the highest-profile language development effort going today. By 
riding the tidal wave of Internet growth, Java is being propagated out to 
systems world-wide. This presents a tempting opportunity to language 
implementors: if a programming language can be compiled for the Java VM, then 
it can be executed on millions of computers. A port to the Java VM is a port 
to practically every important hardware platform in existence. This has, for 
example, drawn the attention of DARPA, where program managers have expressed 
interest in having the results of programming language research targeted to 
the Java VM as a means of doing instantaneous technology transfer to the 
commercial sector.

However, the Java VM is not a good target machine for dynamic languages, for 
reasons involving efficient representations of both data and computation in 
dynamic languages. In this note, we'll consider some of the difficulties 
imposed by the current VM specification, and examine two extensions to 
alleviate these problems.
The data representation problem

Dynamic languages typically require special representations for their data 
values. Consider, for example, the requirements imposed by Scheme's 
polymorphism and dynamic type system. Scheme's polymorphism requires a 
"uniform representation" of data. For example, the car and cdr slots of a cons 
cell can contain any value of any type, so all values of all types must be 
able to fit into the storage allocated for the slots of the cons cell. 
Similarly, the formal parameter of a procedure can be bound to any value of 
any type. This means that subroutine linkage conventions must be able to 
accept any value in the expected place, whether it is a character, integer, 
array, or other value.

Scheme implementations solve the one-size-fits-all problem by representing all 
values with exactly one machine word:

     * If the value is smaller than a single word (e.g., a character or 
boolean), it is padded to fit into a single word. Some of the word's bits are 
used to represent the type of the value. Let us call these values "immediate 
descriptors."
     * If the value is larger than a single word (e.g., a cons cell or 
double-precision floating-point number), it is "boxed" -- that is, the actual 
data for the value is stored in memory, and the value is represented by the 
address of that region of memory. The bit-encoding of the address needs to be 
distinguishable from bit-encoding of immediate descriptors; beyond this simple 
discrimination, more complex type information can be stored in the header of 
the associated block of memory.

Unfortunately, the Java virtual machine has been designed as a target for a 
monomorphic, statically-typed object-oriented language. To the degree that 
polymorphism is supported by the virtual machine, it must be realised by the 
class hierarchy, which allows us to build objects of different run-time "types."

For example, suppose we wish to compile Scheme code for the Java virtual 
machine. The different types of Scheme values can be represented as 
sub-classes of the Java Object class: we can represent small integers as Java 
Int objects; define a Procedure class for Scheme procedures, with Closure and 
Primop sub-classes; and so forth. The VM allows one to perform run-time tests 
to query if an object is of a given sub-class, so we can perform the run-time 
type tests that Scheme needs as desired.

The difficulty with this representation is that small objects, such as 
characters, small integers, and booleans, must be represented as general Java 
objects. The Java virtual machine uses a boxed representation to be able to 
uniformly describe objects of different classes: an object is represented by a 
pointer to a chunk of memory containing a description of its class and its 
state variables. As with Scheme data, boxing Java object data allows us to 
represent all Java objects uniformly, with exactly one machine word.

The overhead of boxing is acceptable for multi-word objects, but is quite 
expensive for objects that fit within a single word. Implementations of 
dynamic languages typically use clever encodings to represent these values as 
immediate descriptors -- allowing them to express the value in a single 
machine word, without requiring any extra information to be stored in memory.

Immediate descriptors and pointers to boxed values are frequently 
discriminated by using a single low-order bit of the descriptor. Since backing 
store for boxed objects is typically allocated on word-size boundaries, on a 
byte-addressable machine the low-order bit is unused by pointers, and so can 
be used for this low-level discrimination.

For example, some Scheme implementations represent immediate data (such as 
characters, 30-bit integers, the empty list, and booleans) as 32-bit patterns 
whose least-significant bit is zero. The next bit is used to discriminate 
30-bit integers from other immediate descriptors: if it is zero, the 32-bit 
pattern represents a 30-bit integer; if it is one, the next six bits are used 
to provide the type information for the descriptor, leaving three bytes from 
the descriptor for the actual data.

Note that making the small-integer type tag be the bits "00" means that we can 
add and subtract these values without having to convert to the raw machine 
representation and back; multiplication requires one normalising shift before 
the multiply; division requires a shift after the divide.

Scheme, Lisp, Dylan, and Smalltalk implementors have exploited these sorts of 
representational devices for decades to achieve good performance from their 
systems.

Without immediate-descriptor representations, the boxing/unboxing costs can 
swamp the actual cost of computing with small values. For example, adding two 
small integers would involve two memory fetches to unbox the addends, an add 
instruction, and then an allocation to box the new value. String and character 
processing will have similar overheads. In short, while our large, composite 
data structures remain fairly efficient, it becomes much more expensive to 
compute with the primitive scalar data values.
Adding immediate descriptors to the Java VM

A simple extension to the Java VM will allow us to reap the benefits of 
immediate descriptors. We can do this by making two, simple, 
backwards-compatible changes to the VM:

     * All pointers to boxed Java objects must have a low bit of one. This is 
not an onerous restriction: if boxed objects are allocated in word units, on a 
byte-addressable machine the low two bits of the pointer are unused.
     * A new class, ImmediateDescriptor, is introduced. ImmediateDescriptor is 
a direct sub-class of the Object class, and has exactly 31 bits of state (we 
might also wish to define a LongImmediateDescriptor class with 63 bits of 
state). The ImmediateDescriptor class is a final class (meaning that it cannot 
be sub-classed), so if a Java object is known to be of class 
ImmediateDescriptor, it requires no more than 31 bits to represent its value.

       An ImmediateDescriptor object is represented as a machine word with low 
bit 0. It requires no backing storage, and can reliably be distinguished from 
boxed objects by its low bit.

An ImmediateDescriptor can be cast to an even 32-bit integer; such a cast is 
merely a compile-time change of viewpoint, requiring no run-time computation. 
Similarly, an integer can be cast to an ImmediateDescriptor; the Java VM 
implements this cast by shifting the integer left one bit.

Implementations of dynamic languages can now use immediate descriptors to 
efficiently represent small data values without requiring the allocation of 
backing store.

Introducing immediate descriptors into the VM should have almost no impact on 
programs that do not use them. It is worth examining, however, the task of 
method lookup on Java objects for performance impact. Method lookup of 
standard Java objects involves indirecting through the pointer that represents 
the boxed object. Such an indirection is not well-defined for descriptors that 
are not addresses, such as immediate descriptors. So the Java machine must 
perform method lookup on objects of class ImmediateDescriptor using an 
alternate technique, for example, by checking in a special method table known 
to the VM.

If an object is known to be any sub-class of the Object class that isn't the 
ImmediateDescriptor class, then it is guaranteed to be boxed, and the standard 
method lookup can be employed. If an object is known to be an 
ImmediateDescriptor, then the special method-lookup technique can be used 
instead. However, if the object is of class Object, then it could be either 
boxed or immediate, and the VM must perform a run-time test on the low bit to 
determine how to look up its method. Fortunately, we can use a classic Lisp 
trick to optimise this case: since the two kinds of descriptor are 
distinguished by their low bit, the VM can simply assume the descriptor is a 
boxed object and perform the necessary memory load to start the method lookup. 
If the descriptor is actually an immediate descriptor, this will generate a 
memory-alignment exception on the underlying hardware. The VM can catch this 
exception and vector off to the custom code for doing method lookups on 
immediate descriptors. With this trick, supporting immediate descriptors in 
the VM imposes zero cost on operations that are performed on boxed Java objects.
The computational representation problem

Now let us turn to the problem of using the Java VM to represent the 
computations to be performed upon our data. The problem here is to be found in 
the design of the instruction set.
Encoding tension: trust vs. efficiency

The basic issue addressed by the design of the Java VM is the tension between 
trust and efficiency. We wish to describe our computation with a notation 
(that is, a language) that

     * can be verified locally, and
     * is efficiently executable.

The Java VM's instruction set fits these criteria. It fits the second 
criterion because it was carefully designed to efficiently encode programs 
expressed in Java. Now that we are trying to extend the coverage to other 
programming languages, we are having trouble meeting the efficiency criterion 
-- the Java VM does not efficiently express Scheme or Dylan programs. What to 
do? If we changed the VM to be a generic RISC instruction set, we'd get our 
efficiency back, but we'd lose the safety criterion. How do you trust a 
program expressed in assembler? That is the tension.

The essence of the problem with the Java VM as a general-purpose instruction 
set is that it is a CISC: a high-level, highly encoded instruction set that is 
carefully tuned to the demands of the Java language. The individual building 
blocks -- the instructions -- for expressing a computation are complex, 
high-level, safe units. For example, the Java VM "hard-wires" method lookup 
into a single instruction.

The problem with CISC instruction sets is that they are brittle encodings. If 
a computation fits the instruction set exactly, things are good: it can be 
encoded compactly, and executed efficiently. But if a computation is just 
slightly different, there is no simple, efficient encoding. For example, if 
our object-oriented language does method lookup just a little bit differently 
from the Java semantics that are implemented by the VM's instruction set, we 
can't use the VM's class system or the VM's method-lookup instruction.

This is not the fault of the Java VM. It was tuned for implementing Java and 
cleverly exploits this constraint to achieve dense, efficient encodings of 
programs written in Java. The problem is that now the VM is being used for new 
purposes -- as the target of different languages.

This limitation of CISC instruction sets has always been at the heart of the 
RISC philosophy: encoding programs at a lower-level means there is more room 
to maneuver when the compiler is mapping high-level programs down to the 
machine in ways that efficiently utilise the machine's resources. The lesson 
here is that this holds as much for the Java VM in the nineties as it did for 
the VAX 11/780 in the eighties.
Writeable control store and trust boundaries

Fahlman has made an interesting proposal to help "open up" the Java VM's 
instruction set in order to address this problem. The idea is to allow some of 
the opcode space in the VM's instruction set to be implemented by C routines 
that are dynamically linked into the VM. The implementors for a given language 
could therefore design and use the extra handful of instructions that 
efficiently express computations written in their language. This is 
essentially the VM analog of writeable control store -- extensible microcode.

What we are doing here is playing with the trust boundaries to improve the 
efficiency of the whole system. The microcode extension allows us to define 
new safe building blocks using a dangerous implementation substrate. However, 
allowing for "microcode" written in C and delivered as raw machine code to be 
dynamically loaded into the VM requires us to decide why we are going to trust 
the microcode, and how we are going to verify programs that use these 
instructions.

This technique exploits the fact that there aren't very many language 
implementations -- there are many more programs than there are languages. A 
handful of microcode libraries would need to be developed by the language 
community. Perhaps we would end up with one from Harlequin for doing Dylan 
(with Dylan-style method lookup in an instruction), one from Hewlett-Packard 
for doing Scheme (with closures, tail-recursion, and tagged arithmetic), and 
so forth. The Dylan implementors at CMU might either collaborate with the 
Harlequin team on a common instruction-set extension, or independently develop 
one tuned for their compiler technology. The source code for these few 
microcode libraries would be submitted to a Java consortium, who would check 
them and publish digitally-signed copies on the network. When an application 
is loaded by the Java VM, if the VM doesn't have the necessary microcode 
loaded, it fetches it from a server and verifies the consortium's digital 
signature. If the application is implemented using some experimental system 
which hasn't yet received the consortium's imprimatur, the user would have the 
option of loading in the microcode from other routes -- perhaps he got a 
floppy disk from CMU with their Dylan release that he trusts.

To repeat, notice that the issue is: why should I trust your microcode, which 
comes written in some dangerous, unverifiable language, such as a SPARC a.out 
file or a chunk of C code? The example answers I have been giving rely on the 
fact that the scale of the problem is very containable, since it per language 
implementation, not per program. If the user will deal with the mechanics of 
authorising his computer to trust Hewlett-Packard's Scheme microcode, he can 
now run any Scheme application in the world that was produced with 
Hewlett-Packard's Scheme-to-Java-VM compiler.

This little extra degree of freedom means that language implementors such as a 
Dylan development team could design those few extra instructions that would 
make Dylan run well on the VM -- exploiting the greater freedoms of a 
dangerous implementation substrate such as C.

Note that this technique is mostly concerned with describing programs, not 
data. It doesn't help us tune data representations. For example, to address 
the Scheme arithmetic boxing problem, we need the immediate-descriptor 
technique described earlier.
Compiler-oriented microcode

Fahlman's proposal provides an extensible method for breaking free of the 
limits imposed by the current VM's CISC, Java-focused design. However, there 
is a serious problem with the proposal as it stands: it is oriented towards 
interpreter implementations of the VM, not compiler (or other) 
implementations. This limits efficiency. The Java byte-codes are designed to 
allow them to be translated "on-the-fly" into efficient native code when they 
are loaded off the network. But our new instructions are described to the Java 
engine with C code or some routines that are delivered to the VM simply as a 
chunk of raw native machine code. How will the VM's byte-code translator be 
able to translate the use of such an instruction to native code?

A simple answer is just to translate each use of the instruction into a 
subroutine call to the machine-code subroutine that defines it. This technique 
is adequate for very large-granularity instructions, such as FFT (to choose an 
extreme example), or perhaps method lookup in some complex object-oriented 
language. The overhead of the subroutine call will be amortised by the time 
spent executing the instruction. This overhead is unacceptable, however, for 
"little" instructions, such as an overflow-checking add-with-trap instruction. 
Unfortunately, these instructions are also quite important for efficient 
implementations of dynamic languages.

This leads us to a compiler-oriented variant of Fahlman's microcode proposal. 
Let us define some lower-level machine underlying the VM -- such as a generic 
RISC processor with operations described in a simple RTL language. For 
example, we could use an RTL representation similar to the one that is 
employed for the intermediate representation of the gcc compiler. Note that 
the RTL language is a dangerous language, not a restricted, safe one -- it is 
the dangerous, trusted machinery we use to define our safe building blocks.

New instructions are defined with macros that expand from a use of the 
instruction to its implementation in RTL. If the instruction is simple, such 
as add-with-trap, then the macro expands into a small quantity of RTL which 
directly performs the operation in toto. If the instruction is complex, such 
as method lookup for an object-oriented language, then the macro expands into 
a subroutine call to a shared microcode routine that does the operation. The 
shared microcode routine is also described with the dangerous RTL sublanguage.

Now an application that uses instruction-set extensions has multiple 
implementation possibilities:

     * A compiling implementation that runs on a stock microprocessor can 
expand instruction uses appearing in the byte-codes into RTL and then 
translate to native code using standard compiler technology. (The basic Java 
VM instructions can be specially handled by the translator for fast 
translation.) We trust the macro-expanders to generate safe uses of the 
dangerous RTL -- that is the trust requirement, and it is also the little 
extra degree of freedom that gives us efficiency.
     * A hardware Java engine would simply implement the RTL engine as well.
     * A byte-code interpreter that does not translate to native code can 
either expand all the extended instructions to RTL, or macro-expand a 
canonical code sequence for a particular instruction (such as add-with-trap), 
producing a little subroutine for the interpreter to call on each use of that 
instruction.

We can limit our macro language as we please -- it doesn't have to be Turing 
equivalent.

The main point of this proposal is that if we choose to make the Java VM 
extensible, we must be careful in choosing how we express these extensions. 
The extensions should be described in some form that is amenable to a spectrum 
of implementation strategies; a form which our computer systems can manipulate 
efficaciously. C source and processor-specific machine code do not satisfy 
these requirements.

This extension mechanism is clearly less well-developed than the 
immediate-descriptor technique presented in the first half of this note. Much 
detail needs to be developed, such as the exact design of the RTL language 
used for defining extended instructions, or the associated instruction 
meta-information used to describe to the byte-code verifier the static type 
constraints of the defined instructions.
Conclusion

Using the Java VM as a propagation vector for distributing dynamic languages 
out to a large audience is a tempting goal. We've examined two difficulties 
with targeting dynamic languages to the current Java VM: the overhead of 
boxing small data structures, and the mismatch between the VM's Java-tuned 
instruction set and the requirements of dynamic languages. The former problem 
can be addressed with a simple, backwards-compatible extension to the VM. It 
appears that this extension should have no performance impact on current Java 
programs. The latter problem can be addressed with the more speculative 
proposal of adding an extension mechanism to the VM that is amenable to 
compiler processing.
Bibliography
[ALIT]
Peter Lee (editor).
Topics in Advanced Language Implementation.
MIT Press, 1991.

[Dylan]
Dylan: An Object-Oriented Dynamic Language.
Apple Computer, 1992.

[Fahlman96]
Scott E. Fahlman.
Email to the [email protected] mailing list, January 10, 1996. The 
java-vm mailing list is archived at the MIT AI Lab.

[Java]
Java: Programming for the Internet.
Sun Microsystems, 1995.

[Scheme]
J. Rees and W. Clinger (editors).
The revised^3 report on the algorithmic language Scheme.
SIGPLAN Notices 21(12):37-79, December 1986.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.