EECS665 – Compiler Construction – Drew Davidson $\newcommand{\bigsqcap}{\mathop ⨅}$

    Parameter Passing


    Contents

    Overview

    In a Java program, all parameters are passed by value. However, there are three other parameter-passing modes that have been used in programming languages:

    1. pass by reference
    2. pass by value-result (also called copy-restore)
    3. pass by name
    We will consider each of those modes, both from the point of view of the programmer and from the point of view of the compiler writer.

    First, here's some useful terminology:

    1. Given a method header, e.g.:
             void f(int a, boolean b, int c)
        
      we will use the terms parameters, formal parameters, or just formals to refer to a, b, and c.

    2. Given a method call, e.g.:
             f(x, x==y, 6);
        
      we will use the terms arguments, actual parameters, or just actuals to refer to x, x==y, and 6.

    3. The term r-value refers to the value of an expression. So for example, assuming that variable x has been initialized to 2, and variable y has been initialized to 3:
      expression r-value
      x 2
      y 3
      x+y 6
      x==y false

    4. The term l-value refers to the location or address of an expression. For example, the l-value of a global variable is the location in the static data area where it is stored. The l-value of a local variable is the location on the stack where it is (currently) stored. Expressions like x+y and x==y have no l-value. However, it is not true that only identifiers have l-values; for example, if A is an array, the expression A[x+y] has both an r-value (the value stored in the x+yth element of the array), and an l-value (the address of that element).
    L-values and r-values get their names from the Left and Right sides of an assignment statement. For example, the code generated for the statement x = y uses the l-value of x (the left-hand side of the assignment) and the r-value of y (the right-hand side of the assignment). Every expression has an r-value. An expression has an l-value iff it can be used on the left-hand side of an assignment.

    Value Parameters

    Parameters can only be passed by value in Java and in C. In Pascal, a parameter is passed by value unless the corresponding formal has the keyword var; similarly, in C++, a parameter is passed by value unless the corresponding formal has the symbol & in front of its name. For example, in the Pascal and C++ code below, parameter x is passed by value, but not parameter y:

      // Pascal procedure header
      Procedure f(x: integer; var y: integer);
      
      // C++ function header
      void f(int x; int & y);
      
    When a parameter is passed by value, the calling method copies the r-value of the argument into the called method's AR. Since the called method only has access to the copy, changing a formal parameter (in the called method) has no effect on the corresponding argument. Of course, if the argument is a pointer, then changing the "thing pointed to" does have an effect that can be "seen" in the calling procedure. For example, in Java, arrays are really pointers, so if an array is passed as an argument to a method, the called method can change the contents of the array, but not the array variable itself, as illustrated below:
      void f( int[] A ) {
          A[0] = 10;   // change an element of parameter A
          A = null;    // change A itself (but not the corresponding actual)
      }
      
      void g() {
          int[] B = new int [3];
          B[0] = 5;
          f(B);
          //*** B is not null here, because B was passed by value
          //*** however, B[0] is now 10, because method f changed the first element
          //*** of the array pointed to by B
      }
      


    TEST YOURSELF #1

    What is printed when the following Java program executes and why?

      class Person {
          int age;
          String name;
      }
      
      class Test {
          static void changePerson(Person P) {
              P.age = 10;
      	P = new Person();
      	P.name = "Joe";
          }
      
          public static void main(String[] args) {
              Person P = new Person();
      	P.age = 2;
      	P.name = "Ann";
      	changePerson(P);
      	System.out.println(P.age);
      	System.out.println(P.name);
          }
      }
      

    solution


    Reference Parameters

    When a parameter is passed by reference, the calling method copies the l-value of the argument into the called method's AR (i.e., it copies a pointer to the argument instead of copying the argument's value). Each time the formal is used, the pointer is followed. If the formal is used as an r-value (e.g., its value is printed, or assigned to another variable), the value is fetched from the location pointed to by the pointer. If the formal is assigned a new value, that new value is written into the location pointed to by the pointer (the new value is not written into the called method's AR).

    If an argument passed by reference has no l-value (e.g., it is an expression like x+y), the compiler may consider this an error (that is what happens in Pascal, and is also done by some C++ compilers), or it may give a warning, then generate code to evaluate the expression, to store the result in some temporary location, and to copy the address of that location into the called method's AR (this is is done by some C++ compilers).

    In terms of language design, it seems like a good idea to consider this kind of situation an error. Here's an example of code in which an expression with no l-value is used as an argument that is passed by reference (the example was actually a Fortran program, but Java-like syntax is used here):

            void mistake(int x) {  // x is a reference parameter
    	   x = x+1;
    	}
    
            void main() {
               int a;
    	   mistake(1);
    	   a = 1;
               print(a);
            }
    
    When this program was compiled and executed, the output was 2! That was because the Fortran compiler stored 1 as a literal at some address and used that address for all the literal "1"s in the program. In particular, that address was passed when "mistake" was called, and was also used to fetch the value to be assigned into variable a. When "mistake" incremented its parameter, the location that held the value 1 was incremented; therefore, when the assignment to a was executed, the location no longer held a 1, and so a was initialized to 2!

    To understand why reference parameters are useful, remember that, although in Java all non-primitive types are really pointers, that is not true in other languages. For example, consider the following C++ code:

      class Person {
        public:
           String name;
           int age;
      };
      
      void birthday(Person per) {
         per.age++;
      }
      
      void main() {
         Person P;
         P.age = 0;
         birthday(P);
         print(P.age);
      }
      
    Note that in main, variable P is a Person, not a pointer to a Person; i.e., main's activation record has space for P.name and P.age. Parameter per is passed by value (there is no ampersand), so when birthday is called from main, a copy of variable P is made (i.e., the values of its name and age fields are copied into birthday's AR). It is only the copy of the age field that is updated by birthday, so when the print statement in main is executed, the value that is output is 0.

    This motivates some reasons for using reference parameters:

    1. When the job of the called method is to modify the parameter (e.g., to update the fields of a class), the parameter must be passed by reference so that the actual parameter, not just a copy, is updated.
    2. When the called method will not modify the parameter, and the parameter is very large, it would be time-consuming to copy the parameter; it is better to pass the parameter by reference so that a single pointer can be passed.


    TEST YOURSELF #2

    Consider writing a method to sort the values in an array of integers. An operation that is used by many sorting algorithms is to swap the values in two array elements. This might be accomplished using a swap method:

      static void swap(int x, int y) {
          int tmp = x;
          x = y;
          y = tmp;
      }
      
    Assume that A is an array of 4 integers. Draw two pictures to illustrate what happens when the call:
      swap(A[0], A[1]);
      
    is executed, first assuming that this is Java code (all parameters are passed by value), and then assuming that this is some other language in which parameters are passed by reference. In both cases, assume that the array itself is stored in the heap (i.e., the space for A in the calling method's AR holds a pointer to the space allocated for the array in the heap). Your pictures should show the ARs of the calling method and method swap.

solution


It is important to realize that the code generator will generate different code for a use of a parameter in a method, depending on whether it is passed by value or by reference. If it is passed by value, then it is in the called method's AR (accessed using an offset from the FP). However, if it is passed by reference, then it is in some other storage (another method's AR, or in the static data area). The value in the called method's AR is the address of that other location.

To make this more concrete, assume the following code:

    void f(int a) {
       a = a - 5;
    }
    
    void main() {
        int x = 10;
        f(a);
    }
    
Below is the code that would be generated for the statement a = a - 5, assuming (1) that a is passed by value and (2) assuming that a is passed by reference:

Passed by Value

image/svg+xml lw t0,(FP)sub t0,t0,5sw t0,(FP) t0=t0-5 load a's r-value into t0 load a's r-value into t0 store result into f's AR

Passed by Reference

image/svg+xml lw t0,(FP)lw t1,(t0)sub t1,t1,5sw t1,(t0) t1=t1-5 load a's L-value into t0 store result into main's AR load a's r-value into t1


TEST YOURSELF #3

The code generator will also generate different code for a method call depending on whether the arguments are to be passed by value or by reference. Consider the following code:

        int x, y;
        x = y = 3;
        f(x, y);
    
Assume that f's first parameter is passed by reference, and that its second parameter is passed by value. What code would be generated to fill in the parameter fields of f's AR?

Value-Result Parameters

Value-result parameter passing was used in Fortran IV and in Ada. The idea is that, as for pass-by-value, the value (not the address) of the actual parameters are copied into the called method's AR. However, when the called method ends, the final values of the parameters are copied back into the arguments. Value-result is equivalent to call-by-reference except when there is aliasing (note: "equivalent" means the program will produce the same results, not that the same code will be generated).

Two expressions that have the same l-value are called aliases. Aliasing can happen:

  • via pointer manipulation,
  • when a parameter is passed by reference and is also global to the called method,
  • when a parameter is passed by reference using the same expression as an argument more than once; e.g., call test(x,y,x).
Will will look at examples of each of these below.

Creating Aliases via Pointers

Pointer manipulation can create aliases, as illustrated by the following Java code. (Note: this kind of aliasing does not make pass-by-reference different from pass-by-value-result; it is included here only for completeness of the discussion of aliasing.)

    Person p, q;
      p = new Person();
      q = p;
      // now p.name and q.name are aliases (they both refer to the same location)
      // however, p and q are not aliases (they refer to different locations)
    

    Pictorially:

    image/svg+xml name: age: A Person Object p q

Creating Aliases by Passing Globals as Arguments

This way of creating aliases (and the difference between reference parameters and value-result parameters in the presence of this kind of aliasing) are illustrated by the following C++ code:

    int x = 1;      // a global variable
    
    void f(int & a)
    { a = 2;        // when f is called from main, a and x are aliases
      x = 0;
    }
    
    main()
    { f(x);
      cout << x;
    }
    
As stated above, passing parameters by value-result yields the same results as passing parameters by reference except when there is aliasing. The above code will print different values when f's parameter is passed by reference than when it is passed by value-result. To understand why, look at the following pictures, which show the effect of the code on the activation records (only variables and parameters are shown in the ARs, and we assume that variable x is in the static data area):

Call-by-reference

Call-by-value

At time of call

image/svg+xml x: main's AR a: f 's AR Static DataArea 1
image/svg+xml x: main's AR a: f 's AR Static DataArea 1 1

After a = 2

image/svg+xml x: main's AR a: f 's AR Static DataArea 2
image/svg+xml x: main's AR a: f 's AR Static DataArea 1 2

After x = 0

image/svg+xml x: main's AR a: f 's AR Static DataArea 0
image/svg+xml x: main's AR a: f 's AR Static DataArea 0 2

After call

When f returns the final value of value-result parameter a is copied back into the space for x, so:

image/svg+xml x: main's AR Static DataArea 0
image/svg+xml x: main's AR Static DataArea 2

Output

0

2

Creating Aliases by Passing Same Argument Twice

Consider the following C++ code:

      void f(int &a, &b)
      { a = 2;
        b = 4;
      }
       
      main()
      { int x;
        f(x, x);
        cout << x;
      } 
    
Assume that f's parameters are passed by reference. In this case, when main calls f, a and b are aliases. As in the previous example, different output may be produced in this case than would be produced if f's parameters were passed by value-result (in which case, no aliases would be created by the call to f, but there would be a question as to the order in which values were copied back after the call). Here are pictures illustrating the difference:

Call-by-reference

Call-by-value-result

At time of call

image/svg+xml b: main's AR a: f 's AR x: ?
image/svg+xml b: main's AR a: f 's AR x: ? ? ?

After a = 2

image/svg+xml b: main's AR a: f 's AR x: 2
image/svg+xml b: main's AR a: f 's AR x: ? 2 ?

After b = 4

image/svg+xml b: main's AR a: f 's AR x: 4
image/svg+xml b: main's AR a: f 's AR x: ? 2 4

After call

image/svg+xml main's AR x: 4
image/svg+xml main's AR x: ?

Output

4
???

With value-result parameter passing, the value of x after the call is undefined, since it is unknown whether a or b gets copied back into x first. This may be handled in several ways:

  • Code like this (where multiple actual parameters that are passed by value have the same l-value) may cause a compile-time error.
  • The order in which parameter values are copied back after a call may be defined by the specific language.
  • The order in which parameter values are copied back after a call may be left as implementation dependent (so code like the above may produce different outputs when compiled with different compilers).


TEST YOURSELF #4

Assume that all parameters are passed by value-result.

Question 1: Give a high-level description of what the code generator must do for a method call.

Question 2: Give the specific code that would be generated for the call shown below, assuming that variables x and y are stored at offsets -8 and -12 in the calling method's AR.

    int x, y;
    f(x, y);
    

Name Parameters

Call-by-name parameter passing was used in Algol. The way to understand it (not the way it is actually implemented) is as follows:

  • Every call statement is replaced by the body of the called method.
  • Each occurrence of a parameter in the called method is replaced with the corresponding argument -- the actual text of the argument, not its value.
For example, given this code:
image/svg+xml void Init(int x, int y){ for (int k = 0; i < 10; k++){ y = 0; x++; } }main(){ int j; int A[10]; j = 0; Init(j, A[j]);}
The following shows this (conceptual) substitution, with the substituted code in the dashed box:
image/svg+xml main(){ int j; int A[10]; j = 0; for (int k = 0; i < 10; k++){ A[j] = 0; j++; }} Actual A[j] for formal y Actual j for formal y

Call-by-name parameter passing is not really implemented like macro expansion however; it is implemented as follows. Instead of passing values or addresses as arguments, a method (actually the address of a method) is passed for each argument. These methods are called 'thunks'. Each 'thunk' knows how to determine the address of the corresponding argument. So for the above example:

  • 'thunk' for j - return address of j
  • 'thunk' for A[j] - return the address of the jth element of A, using the current value of j
Each time a parameter is used, the 'thunk' is called; then the address returned by the 'thunk' is used.

For the example above, call-by-reference would execute A[0] = 0 ten times, while call-by-name initializes the whole array!

The effect of evaluating argument expressions in the callee as needed can have some surprising effects. For example, an argument that would otherwise cause a runtime crash (say divide-by-zero) won't cause any problems until it is actually used (if at all). Factors like these often make call-by-name programs hard to understand - it may require looking at every call of a method to figure out what that method is doing.

Call-by-name is interesting for historical, research, and academic reasons, However, it is considered too confusing for developers in practice and industry has largely passed it by in favor of call-by-value or call-by-reference.

Comparisons of These Parameter Passing Mechanisms

Here are some advantages of each of the parameter-passing mechanisms discussed above:

Call-by-Value (when not used to pass pointers)

  • Doesn't cause aliasing.
  • Arguments unchanged by method call, so easier to understand calling code (no need to go look at called method to see what it does to actual parameters).
  • Easier for static analysis (for both programmer and compiler). For example:
            x = 0;
            f(x);           {call-by-value so x not changed}
            z = x + 1;      {can replace by z = 1 when optimizing}
    	
  • Compared with call-by-reference, the code in the called method is faster because there is no need for indirection to access formals.

Call-by-Reference

  • More efficient when passing large objects (only need to copy addresses, not the objects themselves).
  • Permits actuals to be modified (e.g., can implement swap method for integers).

Call-by-Value-Result

  • As for call-by-value, more efficient than call-by-reference for small objects (because there is no overhead of pointer dereferencing for each use).
  • If there is no aliasing, can implement call-by-value-result using call-by-reference for large objects, so it is still efficient.

Call-by-Name

  • More efficient than other approaches when passing parameters that are never used. For example:
    image/svg+xml f(Ackermann(4,2),0);void f(int a, int b){ if (b == 1){ return a + 1; } else { return 0; }} The Ackermann function takes enormous time tocompute
    If the condition b in method f is not 1, then using call-by-name, it is never necessary to evaluate the first actual at all! That's good because doing so would take a long time (The Ackermann function is famously slow to evaluate. In this example, the result is an integer of nearly 20,000 digits).