Procedure Call and Return
Total Page:16
File Type:pdf, Size:1020Kb
Chapter 11 Procedure Call and Return This chapter discusses the design of code fragments which support procedure call and return in both recursive and non-recursive programming languages. To aid the discussion in later chapters it includes a model of procedure call and return based on the notion of a procedure activation record. A procedure activation is a special case of a `micro-process',1 and the same model can explain the operation of languages such as SIMULA 67 whose control structures are not restricted to simple procedure call and return. It also helps to explain how restrictions on the use of data and procedures in the source language can allow restricted (and therefore less costly) implementations of the general procedure call and return mechanism. The mechanism of procedure call and return in FORTRAN is briefly discussed: then I concentrate on the mechanisms of stack handling which are required to implement procedure call and return in a recursive language on a conventional object machine. 11.1 The Tasks of Procedure Call and Return Fragments Each procedure body in the source program is represented by a section of in- structions in the object program. When a procedure has been called, and before it returns, various items of information represent the state of the current pro- cedure activation. The instruction pointer defines a position in the code, the contents of the data frame (see chapters 1 and 8) define the values both of the parameters and of any data objects owned by the activation, and an explicit or implicit collection of pointers defines the activation's environment { those non-local data frames which the current procedure activation may access. Note the distinction between the instructions of the procedure body, the data frame set up when the procedure is called and the procedure activation proper, which 1 `Micro' process because a process in many languages can be made up of many micro- processes. 193 194 CHAPTER 11. PROCEDURE CALL AND RETURN Call 1. Allocate space for the data frame (activation record) of the called proce- dure. 2. Prepare information about the arguments of the procedure call and place it in the parameter slots of the new data frame (or in some pre-arranged locations). 3. Set up environment pointers to allow the new activation access to other data frames (e.g. those of textually enclosing blocks or procedures in block- structured languages). 4. Save the current data frame/ environment/ instruction pointers and create new values for the new activation. 5. Stop execution of the current procedure activation and start that of the new (called) activation. Return 1. Store the procedure result in a pre-arranged register or location. 2. Reload the calling procedure activation's data frame/ environment/ in- struction pointers. 3. Stop execution of the called procedure activation and resume execution of the calling activation. Garbage Collection Reclaim the space allocated for the data frame (activation record) of the called procedure. Figure 11.1: Tasks of procedure call and return fragments 11.1. TASKS OF PROCEDURE CALL AND RETURN 195 is defined by a position in the code together with the contents of the data frame and linkages to other data frames (activation records). When execution of the procedure body reaches a point at which the current pro- cedure calls another procedure, or calls itself recursively, the current activation is halted, the items of information which record its state are saved and similar information is set up for the newly created procedure activation. In a recursive language the action of calling a procedure involves the acquisition of space for the data frame of the new activation, usually taken from a stack. When execution of the new activation reaches a point in the procedure body which corresponds to a procedure return, the stored items of information which defined the state of the calling activation are reloaded, and execution of the calling procedure's instruction sequence continues from the point indicated by the instruction pointer. In a recursive language the space acquired for the data frame of the returning procedure activation must be recovered at some stage { usually this means adjusting the stack during or after the sequence of instruc- tions which implement the procedure return. Manipulating data frame pointers, acquiring and releasing data frame space are all part of the maintenance of the Activation Record Structure introduced in chapters 1 and 4. The tasks which must be carried out during execution of procedure call and return are summarised in figure 11.1. Efficiency of execution comes not only from careful design of the individual code fragments which carry out these tasks but also from careful design of the source language to allow short-cuts in the execution of the object program. Restrictions on the source language may re- duce its expressive power but can increase the efficiency of execution of object programs. In FORTRAN, for example, the allocation of data space can be performed once and for all at load-time when the program is loaded and before it is run. Procedure call and return are then more efficient (although not so much more efficient as you might expect { see the examples below and in chapter 13) but the penalty is that FORTRAN programmers can't use the expressive power of recursion. You can't write in FORTRAN, for example, the sort of recursive procedures which are used in this book to illustrate the translation of a parse tree into object code. Conventional recursive languages are also restricted, though less so than FOR- TRAN, in order that data frames can be allocated from a stack. In PASCAL and ALGOL 60 the type of result which can be returned from a procedure or as- signed to a location is restricted so that there can be no pointers to a procedure activation's data frame when it returns; ALGOL 68 imposes similar restrictions but in a less severe form (see chapter 14 for a discussion of the problems this causes). Only SIMULA 67 among the popular compiled languages uses the fully general call and return mechanism, as part of the implementation of classes, and as a result allows the most general form of programming. The cost of generality is relative inefficiency { simple programs tend to run slower in SIMULA than 196 CHAPTER 11. PROCEDURE CALL AND RETURN call declaration argument prologue preparation procedure precall body housekeeping epilogue JSUB ..... postcall RETN ...... housekeeping Figure 11.2: Layout of code fragments in procedure call and declaration they might in, say, PASCAL { but the benefit of generality is that new ways of programming are made possible. In particular SIMULA 67 allows co-processing, which may prove as much of an influence on program design in the future as recursion has proved in the past. Figure 11.1 does not include the checking of compatibility between arguments and parameter specifications (number of arguments, type, kind, etc.) as a task of the object program. Such checks are rarely necessary, are expensive to exe- cute and are too often included in place of more convenient and more efficient compile-time checks. Chapter 12 concentrates on the topic of argument pass- ing and on ways of avoiding run-time argument/parameter checks in modern languages. FORTRAN is the major language in current use which requires run- time argument/parameter checks and yet in implementations of FORTRAN, for the sake of efficiency, such checks are almost never included. 11.2 Layout of Procedure Call and Return Fragments The number and complexity of the tasks set down in figure 11.1 suggest already that it takes more than a single JSUB instruction to call a procedure and more than a single RETN instruction to return to the calling procedure. The layout of code fragments in the object program is illustrated in figure 11.2: the fragments in the calling procedure would be generated as part of the translation of an expression or a statement and the fragments in the called procedure as part of the translation of the procedure declaration. 11.3. RECURSION AND THE `EFFICIENCY' OF FORTRAN 197 procedure printint(num : integer); begin if num>10 then printint(num div 10); printdigit(num mod 10) end; Figure 11.3: Printing an integer main program: CALL A CALL B CALL C CALL D STOP END data frames: main A B C D program Figure 11.4: Space allocation at load time The cost of executing a procedure call is therefore the cost of executing all the fragments shown in figure 11.2 { argument preparation, precall housekeeping, JSUB, prologue, procedure body, epilogue, RETN and postcall housekeeping. Min- imising the cost of a procedure call means minimising this total execution cost. In most cases the cost is reduced by shortening and speeding-up the operation of the run-time support fragments { the `overheads' of the procedure call { but it may occasionally be worthwhile to increase the overhead cost in order to speed up the execution of the procedure body. Examples in which this is so are given in the discussion below of `call-by-result' in FORTRAN and in the discussion in chapter 13 of the relative merits of environment links and display vectors for non-local data addressing. In order to minimise object program size, as many as possible of the tasks defined in figure 11.1 should be performed in the called procedure (i.e. in the prologue and epilogue fragments) rather than in the fragments which occur in the calling procedure. Since there are surely never more procedure declarations than there are procedure calls in the source program, shortening the procedure call at the expense of the prologue and epilogue must lead to a smaller object program.