Experiences using GCJ as an embedded compiler

Jon Olson olson@mmsi.com
Mon Mar 29 08:59:00 GMT 1999


For about the past 4 months I've been working with, on, and underneath
the hot new GCJ frontend for compiling Java class files to native code.
Before I enumerate its current weaknesses, let me thank the people at
cygnus for this wonderful tool. I've used this compiler to build an
entire Java run-time environment and, even though I'm still using the
original 980906 alpha version, have found it to produce quite reliable
and efficient code.
For an embedded environment, GCJ currently has numerous deficiencies.
I've overcome most of these with quite nice implementations and hope
that sometime in the future these changes can be integrated into the
released version of GCJ.
 1) Lightweight exceptions
 I wrote a lengthy email previously on the current implementation
 of exceptions in `gcc' and why they are not usable in an embedded
 environment. I developed a very lightweight exception implementation
 which drastically decreases the overhead required for exception
 support.
 2) Write barriers
 Embedded environments typically have real-time constraints to which
 they must respond. Without write barriers, all threads which modify
 the collected heap must be stopped until collection completes. The
 only threads which can be higher priority than garbage collection are
 those which modify only simple, pointerless data structures.
 Fortunately, write-barriers were relatively simple to add to GCC.
 The following code, added to `gcc/expr.c', invokes a new `write_barrier'
 RTL pattern whenever it writes a pointer to memory.
 #if defined (HAVE_write_barrier)
 /*
	 * Generate write barrier code if we're setting memory with
 * something that's not a constant, not a vtable, and with
 * something that is a pointer.
 */
 else if (flag_write_barriers
 	 && GET_CODE (target) == MEM
	 && GET_MODE (target) == Pmode
 #ifdef TARGET_NEEDS_WRITE_BARRIER
	 && TARGET_NEEDS_WRITE_BARRIER (target)
 #endif
	 && !(TREE_CODE(exp) == ADDR_EXPR
		 && DECL_VIRTUAL_P (TREE_OPERAND (exp, 0)))
	 && !really_constant_p (exp)
	 && contains_pointers_p (TREE_TYPE (exp))) {
 	 temp = force_reg (Pmode, temp);
	 emit_insn (gen_write_barrier (target, temp));
 }
 #endif
 Here's a sample `write_barrier' pattern for the AMD A29K. This
 processor implements write barriers in a particularly elegant
 way. The `write_barrier' register, gr95, normally contains
 0xffffffff. The assert instruction, asleu, asserts that a
 particular pointer being written to memory is less or equal to
 the write barrier register. When GC is inactive, this is always
 true and the write barrier requires only 1 instruction which
 acts as a NOP. When GC begins collecting, the write barrier
 is moved to the start of the heap. During collection, only
 writing pointers higher than the start of heap generate traps
 to the write barrier code which then records the object as
 a live object.
 ;; Garbage collector write barrier
 (define_insn "write_barrier"
 [(parallel [
	 (set (match_operand:SI 0 "memory_operand" "=m")
	 (match_operand:SI 1 "register_operand" "r"))
	 (unspec_volatile [(match_dup 1)] 16)])]
 ""
 "store 0,0,%1,%0\;asleu V_write_barrier,%1,gr95")
 I have similar implementations for StrongARM and Hitachi Super-H,
 although they require more instructions.
 3) JVM `jsr' instructions
 The current GCJ compiler generates pretty bad code for Java `jsr'
 instructions. This is because a `jsr' isn't really a the same as
 a RTL `call' since the `jsr' shares and corrupts the stack frame
 of the caller. Thus, the compiler must trace the `jsr' as if it
 was a simple jump which saves its return address and returns
 using an `indirect_jump'.
 Instead of using a native `call' instruction, GCJ currently computes
 the return label and uses a `jump' instruction. This, however,
 was relatively easy to fix by modifying `build_java_jsr' to use
 a new RTL pattern named `subroutine_jump'. Only trick is you must
 tell `flow.c' that this instruction computes the address its return
 label or else you get invalid code. Here's the modified code from
 `build_java_jsr'.
 /*
 * Java jsr's aren't the same as a normal `call_insn', since
 * they call a local label which doesn't save any registers.
 * The compiler must know this instruction as a `jump_insn'
 * which includes REG_NOTES that declare the return label
 * as being a label which can be jumped to by an indirect_jump.
 */
 rtx return_label = gen_label_rtx ();
 rtx insn = emit_jump_insn (
 gen_subroutine_jump (DECL_RTL (where), return_label));
 REG_NOTES (insn) = gen_rtx_EXPR_LIST (REG_LABEL, return_label,
					 REG_NOTES (insn));
 emit_label (return_label);
 I also added code in for RISC machines `decl.c' that recognizes
 cases where a return address is popped from top of stack and
 optimizing this to access the machine's return address register.
 4) 64-bit code
 GCJ generates pretty bad code for many 64-bit operations.
 Unfortunately, this isn't easy to fix in one place (at least I couldn't
 find a way) and alot of hacking on each .md file to add appropriate
 RTL patterns that optimize what you want it to do. As an example, try
 the following code that Java uses alot to merge two 32-bit integers.
 
 long merge(int hi, int lo) {
 return ((long)hi << 32) | ((long)lo & 0xffffffff);
 }
 On 32-bit architectures, GCJ will generate all the sign extension,
 shifting, and oring operations instead of the desired move of two
 registers ;-( Oh well. I fixed it on the RISC architectures by
 adding alot of RTL patterns and some peepholes but they're very
 specific to this particular operation. It would be nice if GCC handled
 64-bit values a bit more gracefully in general.
 5) Size of compiled metadata
 GCJ generates a very convenient metadata structure that requires
 very little run-time loading. Here's a sample declaration of the
 JvClass object that I gleaned from the GCJ sources.
 class JvClass : public JvObject	// Declaration of a single class, (__CL_name)
 {
 friend class java::lang::Class;
 jref unused_0;
 jutf8 name;		// UTF class name
 jshort accflags;		// Access rights
 jclass superclass;
 jclass arrayclass;	// Class for arrays of these objects
 jref unused_1;
 JvConstants constants;	// Strings and other constants
 JvMethods methods;	// Methods
 JvFields fields;		// Fields
 jdtable dtable;		// Dispatch table for objects of this class
 jclass *interfaces;	// Vector of interfaces implemented by this object
 java::lang::ClassLoader *loader;
 jshort interface_len;	// Number of interfaces in `interfaces' vector
 jbyte state;
 jbyte final;
 };
 Every JvClass then references a vector of constants, a vector of methods,
 a vector of fields, etc.... This JvClass structure can be directly used
 by the run-time environment with minimal loading. In a conventional PC
 or workstation environment in which the compiled data segment lives on
 inexpensive disk storage and gets loaded directly into RAM, this structure
 makes alot of sense.
 Unfortunately, embedded environments typically use much more precious
 flash memory for non-volatile store. Since the JvClass structure must
 be writable and the internal pointers relocated to live in RAM, all the
 metadata must be moved to RAM and fixup records generates. After building
 an entire Java run-time environment, I've discovered that the Java metadata
 and all its associated fixups consumes about 40% of the total binary size.
 In an embedded environment, this metadata exists twice: once in flash,
 once again in RAM.
 My proposal for embedded environments is to serialize the metadata
 into a much more compact format which gets loaded at run-time into
 much the same format that the compiler currently generates. This
 format actually has two advantages:
 a) Significantly decreasing the flash requirements for embedded systems
 b) Decoupling the compiler's generated metadata format from the
 run-time format. Currently, changes to the run-time format
 require changes to the compiler; similarly, changes to the
 compile time format require modifications to the run-time.
 6) Interface calling mechanism
 Currently, the GCJ compiler generates the following run-time call to
 lookup the native code for a given Java interface.
 jnative
 _Jv_LookupInterfaceMethod(jclass cl, jutf8 methodName, jutf8 methodSignature)
 {
 }
 This calling method is a relatively straightforward translation from the
 JVM `invokeinterface' instruction. Unfortunately, this calling convention
 for interfaces is relatively inefficient, since a method name and signature
 must be found at run-time via class introspection. Since Java uses interfaces
 as its preferred method calling mechanism, it is vital that interface
 invokation be FAST.
 Instead, I propose that GCJ layout vtables for each interface and, at
 compile time, determine the interface and vtable index being invoked.
 Thus, the above run-time interface would change to:
 jnative
 _Jv_LookupInterfaceMethod(jclass cl, jclass xface, int vtableIndex)
 {
 }
 Finding an interface would then be merely matching the `xface' against
 the list of interfaces for the given class, and returning the appropriate
 method from its vtable.
 Is anybody currently considering or working on such an implementation???
 If not, this is my next project for GCJ.
-- 
Jon Olson, Modular Mining Systems
	 3289 E. Hemisphere Loop
	 Tucson, AZ 85706
INTERNET: olson@mmsi.com
PHONE: (520)746-9127
FAX: (520)889-5790


More information about the Java mailing list

AltStyle によって変換されたページ (->オリジナル) /