Code for y := x.f on various machines (Compilers)

From Spivey's Corner
Jump to: navigation, search

This program ptr.p is in picoPascal:

type ptr = pointer to record d, e, f, g: integer end;

var x: ptr; y: integer;

  y := x^.f

It declares x as a pointer to a record with four integer fields. Here is the Keiko code compiled for the assignment y := x^.f using the command ppc ptr.p, without optimisation:


As we'll explore in coming weeks, the GLOBAL _x instruction fetches the address of global variable x, and the following LOADW fetches its value, the address of a heap-allocated record. Next in the program, the constant 8 is added to get the address of the field x^.f. Another LOADW fetches the contents of that field, and the final GLOBAL _y and STOREW store the value into the global variable y. Important to note is that the compiler has worked out the layout of the record and translated the reference to field f into the offset 8.

Turning on the peephole optimiser in the compiler with ppc -O rec.p results in more compact code:


Here LDGW _x has the same effect as GLOBAL _x; LOADW, the instrution LDNW 8 is short for CONST 8; OFFSET; LOADW, and STGW _y stands for GLOBAL _y; STOREW. These instructions can be executed by the bytecode interpreter in three cycles rather than seven.

We can translate the program using the compiler from Lab 4 and the command ppc -O2 rec.p to get code for the ARM:

set r0, _x         @ GLOBAL _x
ldr r0, [r0]       @ LOADW
ldr r0, [r0, #8]   @ CONST 8; OFFSET; LOADW
set r1, _y         @ GLOBAL _y
str r0, [r1]       @ STOREW

I've shown the correspondence with Keiko instructions as comments. What we see here is that the ARM back end chooses intructions that cover combinations of one or more Keiko instructions, beginning with something like the unoptimised version where each individual action is specified separately.

Compiling a similar program for the JVM gives significantly different results. Here is the Java program

class rec {
    public int d, e, f, g;

    public static rec x;
    public static int y;

    public void m() {
        y = x.f;

Using javac to compile the program and javap -c to disassemble it, we get the following code for the method m:

getstatic x:Lrec;
getfield f:I
putstatic y:I

Because the JVM is a higher-level virtual machine than Keiko, the Java compiler has not replaced the reference to f with an offset, but leaves the name f explicit in the object code. It's for the interpreter or translator implementing the JVM to decide how it wants to lay out objects of class rec. That gives it more freedom but also more responsibility, and also makes the JVM less flexible: instead of any data structure that can be expressed in terms of address arithmetic, the JVM is restricted to the kinds of object that can be expressed in Java.

Effect of the instruction set r0, _x

In the assembly languageA symbolic representation of the machine code for a program. program, _x is a constant, the address of a piece of storage that is allocated by the directive

.comm _x, 4, 4

The program is compiled on the assumption that this address can be an arbitrary 32-bit constant. The instruction

set r0, _x

moves the value of this constant into register r0. For our purposes, we can take it for granted that it does this, but you may like to know that the ARM assembler gathers all the constants _x, _y, _z from each procedure in the program and makes them into a literal table that it then accesses using PC-relative addressingAn addressing mode that involves adding a ''small'' constant to the value of the ''program counter'' in order to form an address. On the ARM, PC-relative addressing is used to access tables of large constants that are located at the end of the code for each procedure, giving access to the values of these constants without having to embed large constants in instructions.An addressing mode that involves adding a fixed offset to the value of the program counter in order to form an address. On ARM, a large constant that does not fit in the immediate field of an instruction can be loaded into a register using a PC-relative load instruction. The assembler generated such instructions, and automatically lays out a table of literal values, when a programmer uses the syntax <code>ldr r''n'', =''const''</code>.. The literal table for a procedure is placed at the .ltorg directive that the compiler inserts at the end of the procedure.

In order to get the value of variable x, the set instruction must be followed by another that uses the address to get the value:

set r0, _x
ldr r1, [r0]

It seems wasteful to use two instructions for a simple access to a global variable, but global variable access is (or ought to be) fairly rare, and it often occurs in clusters. For example, the two references to y in y := y+1 can be compiled into three instructions by using the address twice:

set r0, _y
ldr r1, [r0]
add r2, r1, #1
str r2, [r0]

Sharing values in this way will be a crucial part of our compiler back-end.

Personal tools