Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Jeffery C.The implementation of Icon and Unicon.2004

.pdf
Скачиваний:
13
Добавлен:
23.08.2013
Размер:
3.05 Mб
Скачать

135

The interpreter stack and C stack shown in this diagram are not to scale compared with the rest of the block. Both are comparatively large; the actual sizes depend on the address space of the target computer.

The first word of the block is the usual title. The next word contains the number of results the co­expression has produced­its "size." Then there is a pointer to the next co­ expression block on a list that is maintained for garbage collection purposes. See Sec. 11.3.4. Following this pointer there are i­state variables: pfp, efp, gfp, argp, ipc, sp, the current program line number, and ilevel.

Then there is a descriptor for the transmitted result, followed by two more descriptors: one for the co­expression that activates this one and one for a refresh block that is needed if a copy of this co­expression block is needed. C state information is contained in an array of words, cstate, for registers and possibly other state information. The array cstate typically contains fifteen words for such information. The C sp is stored in cstate[0]. The use of the rest of cstate is machine­dependent.

Finally, there is an interpreter stack and a C stack. On a computer with a downward­ growing C stack, such as the VAX, the base of the C stack is at the end of the co­ expression block and the interpreter and C stacks grow toward each other. On a computer

136

with an upward­growing C stack, the C stack base follows the end of the interpreter stack.

Stack Initialization. When a co­expression is first activated, its interpreter stack must be in an appropriate state. This initialization is done when the co­expression block is created. A procedure frame, which is a copy of the procedure frame for the procedure in which the create instruction is executed, is placed on the new stack. It consists of the words from argp through the procedure frame marker and the descriptors for the local identifiers. The efp and gfp in the co­expression block are set to zero and the ipc is set to the value given in the argument to the create instruction (L1).

No C state is set up on the new C stack; this is handled when the co­expression is activated the first time. The initial null value for the activator indicates the absence of a valid C state.

Co­Expression Activation. As mentioned previously, coact and coret perform many similar functions­ both save current state information, establish new state information, and activate another co­expression. The current i­state variables are saved in the current co­expression block, and new ones are established from the co­expression block for the co­expression being activated. Similar actions are taken for the C state. Since the C state is machine­dependent, the "context switch" for the C state is performed by a routine, called coswitch, that contains assembly­language code.

The C state typically consists of registers that are used to address the C stack and registers that must be preserved across the call of a C function. On the VAX, for example, the C stack registers are sp, ap, and fp. Only the registers r6 through r11 must be saved for some C compilers, while other C compilers require that r3 through r11 be saved. Once the necessary registers are saved in the cstate array of the current co­ expression, new values of these registers are established. If the co­expression being activated has been activated before, the C state is set up from its cstate array, and coswitch returns to interp. At this point, execution continues in the newly activated co­ expression. Control is transferred to the beginning of the interpreter loop, and the next instruction (from the ipc for the co­expression) is fetched.

However, when a co­expression is activated for the first time, there are no register values to restore, since no C function has yet been called for the new co­expression. This is indicated, as mentioned previously, by a null activator, which is communicated to coswitch by an integer argument. In this case, coswitch sets up registers for the call of a C function and calls interp to start the execution of the new co­expression. Such a call to interp on the first activation of a co­expression corresponds to the call to interp that starts program execution in the co­expression &main for the main procedure. There can never be a return from the call to interp made in coswitch, since program execution can only terminate normally by a return from the main procedure, in &main.

The function coswitch is necessarily machine­dependent. The version for the VAX with the Berkeley 4.3bsd C compiler is an example:

coswitch(ald_cs, new_cs, first) int *ald_cs, *new_cs;

int first;

{

asm(" mavl 4(ap), rO"); asm(" mavI8(ap),r1"); asm(" mavl sp, O(rO)");

137

asm(" mavl fp,4(rO)"); asm(" mavl ap,8(rO)"); asm(" mavl r11, 16(rO)"); asm(" mavl r1 0, 20(rO)"); asm(" mavl r9,24(r0)"); asm(" mavl r8,28(rO)"); asm(" mavl r7,32(rO)"); asm(" mavl r6,36(rO)");

if (first == 0) { /* this is the first activation */ asm(" mavl 0(r1), sp");

asm(" clrl fp"); . asm(" clrl ap"); interp(0, 0);

syserr("interp() returned in caswitch");

}

else {

asm(" movl 0(r1),sp"); asm(" movl 4(r1), fp"); asm(" movl 8(r1),ap"); asm(" movl 16(r1),r11"); asm(" movl 20(r1), r1 0"); asm(" movJ 24(r1), r9"); asm(" movl 28(r1), r8"); asm(" movl 32(r1),r7");

asm(" movl 36(r1),r6");

}

}

The variables old_cs and new_cs are pointers to the cstate arrays for the activating and activated co­expressions, respectively. The value of first is 0 if the co­expression is being activated for the first time. Note that in order to write coswitch it is necessary to know how the first two arguments are accessed in assembly language. For the previous example, old_cs and new_cs are four an eight bytes from the ap register, respectively.

Refreshing a Co­Expression. The operation ^expr0 creates a copy of the co­expression produced by expr0 with its state initialized to what it was when it was originally created. The refresh block for expr0 contains the information necessary to reproduce the initial state. The refresh block contains the original cip for the co­expression, the number of local identifiers for the procedure in which expr0 was created, a copy of the procedure frame marker at the time of creation, and the values of the arguments and local identifiers at the time of creation. Consider, for example,

procedure labgen(s) local i, j, e

i := 1 j := 100

e := create (s || (i to j) || ":")

end

For the call labgen(''L"), the refresh block for e is

138

RETROSPECTIVE: Invocation expressions are more complicated to implement than operators, since the meaning of an invocation expression is not known until it is evaluated. Since functions and procedures are source­language values, the information associated with them is stored in blocks in the same manner as for other types.

The C code that implements Icon functions is written in the same fashion as the code for operators. Procedures have source­language analogs of the failure and suspension mechanisms used for implementing functions and operators. Procedure frames identify the portions of the interpreter stack associated with the procedures currently invoked.

A co­expression allows an expression to be evaluated outside its lexical site in the program by providing separate stacks for its evaluation. The possibility of multiple stacks in various states of evaluation introduces technical problems in the implementation, including a machine­dependent context switch.

EXERCISES

10.1What happens if a call of a procedure or function contains an extra argument expression, but the evaluation of that expression fails?

10.2Sometimes it is useful to be able to specify a function or procedure by means of its string name. Icon supports "string invocation," which allows the value of expr0 in

expr0(exprj, expr2, ..., exprn)

.

to be a string. Thus,

"write"(s)

produces the same result as

139

write(s)

Of course, such a string name is usually computed, as in (readO)(s)

Describe what is involved in implementing this aspect of invocation. Operators also may be invoked by their string names, as in

"+"(i,j)

What is needed in the implementation to support this facility? Can a control structure be invoked by a string name?

10.3If the result returned by a procedure is a variable, it may need to be dereferenced. This is done in the code for pret and psusp. For example, if the result being returned is a local identifier, it must be replaced by its value What other kinds of variables must be dereferenced? Is there any difference in the dereferencing done by pret and psusp?

10.4How is the structure of a co­expression block different on a computer with an upward­growing C stack compared to one with a downward­growing C stack? What is the difference between the two cases in terms of potential storage fragmentation?

140

Chapter 11: Storage Management

PERSPECTIVE: The implementation of storage management must accommodate a wide range of allocation requirements. At the same time, the implementation must provide generality and some compromise between "normal" programs and those that have unusual requirements. Although it is clearly sensible to satisfy the needs of most programs in an efficient manner, there is no way to define what is typical or to predict how programming style and applications may change. Indeed, the performance of the implementation may affect both programming style and applications.

Strings and blocks can be created during program execution at times that cannot be predicted, in general, from examination of the text of a program. The sizes of strings and of some types of blocks may vary and may be arbitrarily large, although practical considerations dictate some limits. There may be an arbitrary number of strings and blocks. The "lifetimes" during which they may be used are arbitrary and are unrelated, in general, to procedure calls and returns.

Different programs vary considerably in the number, type, and sizes of data objects that are created at run time. Some programs read in strings, transform them, and write them out without ever creating other types of objects. Other programs create and manipulate many lists, sets, and tables but use few strings. Relatively few programs use co­ expressions, but there are applications in which large numbers of co­expressions are created.

Since a program can construct an arbitrary number of data objects of arbitrary sizes and lifetimes, some mechanism is needed to allow the reuse of space for "dead" objects that are no longer accessible to the program. Thus, in addition to a mechanism for allocating storage for objects at run time, there must be a storage­reclamation mechanism, which usually is called garbage collection. The methods used for allocation and garbage collection are interdependent. Simple and fast allocation methods usually require complex and time­consuming garbage­collection techniques, while more efficient garbage­collection techniques generally lead to more complex allocation techniques.

Storage management has influences that are far­reaching. In some programs, it may account for a major portion of execution time. The design of data structures, the layout of blocks, and the representation of strings are all influenced by storage­management considerations. For example, both a descriptor that points to a block and the first word of the block contain the same type code. This information is redundant as far as program execution is concerned, since blocks are accessed only via descriptors that point to them. The redundant type information is motivated by storage­management considerations. During garbage collection, it is necessary to access blocks directly, rather than through pointers from descriptors, and it must be possible to determine the type of a block from the block itself. Similarly, the size of a block is of no interest in performing language operations, but the size is needed during garbage collection. Blocks, therefore, carry some "overhead" for storage management. This overhead consists primarily of extra space, reflecting the fact that it takes more space to manage storage dynamically than would be needed if space were allocated statically. Balancing space overhead against efficiency in allocating and collecting objects is a complex task.

Such problems have plagued and intrigued implementors since the early days of LISP. Many ways have been devised to handle dynamic storage management, and some

141

techniques have been highly refined to meet specific requirements (Cohen 1981). In the case of Icon, there is more emphasis on storage management for strings than there is in a language, such as LISP, where lists predominate. Icon's storage­management system reflects previous experience with storage­management systems used in XPL (McKeeman, Homing, and Wortman 1970), SNOBOL4 (Hanson 1977), and the earlier Ratfor implementation of Icon (Hanson 1980). The result is, of course, somewhat idiosyncratic, but it provides an interesting case study of a real storage­management system.

11.1 Memory Layout

During the execution of an Icon program, memory is divided into several regions. The sizes and locations of these regions are somewhat dependent on computer architecture and the operating system used, but typically they have the following form:

run­time system

icode

allocated storage

free space

system stack

This diagram is not drawn to scale; some regions are much larger than others.

The Run­Time System. The run­time system contains the executable code for the interpreter, built­in operators and functions, support routines, and so forth. It also contains static storage for some Icon strings and blocks that appear in C functions. For example, the blocks for keyword trapped variables are statically allocated in the data area of the run­time system. Such blocks never move, but their contents may change. The size of the run­time system is somewhat machine­dependent. About 75,000 bytes (decimal) is typical.

The Icode Region. One of the first things done by the run­time system is to read in the icode file for the program that is to be executed. The data in the icode region, which is produced by the linker, is divided into a number of sections:

code and blocks

record information

global identifier values

global identifier names

static identifier values

strings

The first section contains virtual machine code, blocks for cset and real literals, and procedure blocks, on a per­procedure basis. Thus, the section of the icode region that contains code and blocks consists of segments of the following form for each procedure:

blocks for real literals

blocks for cset literals

procedure blocks

virtual machine instructions

142

Record information for the entire program is in the second section of the icode region. Next, there is an array of descriptors for the values of the global identifiers in the program, followed by an array that contains qualifiers for the names of the global identifiers. These two arrays are parallel. The ith descriptor in the first array contains the value of the ith global identifier, and the ith descriptor in the second array contains a qualifier for its name.

Following the two arrays related to global identifiers is an array for the values of static identifiers. As mentioned in Sec. 2.1.10, static identifiers have global scope with respect to procedure invocation, but a static identifier is accessible only to the procedure in which it is declared.

Unlike cset and real blocks, which are compiled on a per­procedure basis, all strings in a program are pooled and are in a single section of the icode region that follows the array of static identifiers. A literal string that occurs more than once in a program occurs only once in the string section of the icode region.

Data in the icode region is never moved, although some components of it may change at run time. The size of the icode region depends primarily on the size of the corresponding source program. As a rule of thumb, an icode region is approximately twice as large as the corresponding Icon source­language file. An icode file for a short program might be 1,000 bytes, while one for a large program (by Icon standards) might be 20,000 bytes.

Allocated Storage. The space for data objects that are constructed at run time is provided in allocated storage regions. This portion of memory is divided into three parts:

static region

string region

block region

The static region contains co­expression blocks. The remainder of the allocated storage region is divided into strings and blocks as shown. The string region contains only characters. The block region, on the other hand, contains pointers. This leads to a number of differences in allocation and garbage­collection techniques in different regions.

Data in the static region is never moved, but strings and blocks may be. Both the string and block regions may be moved if it is necessary to increase the size of the static region. Similarly, the block region may be moved in order to enlarge the string region.

The initial sizes of the allocated storage regions vary considerably from computer to computer, depending on the size of the user address space. On a computer with a large address space, such as the VAX, the initial sizes are

static region:

20,480 bytes

(5,120 words)

string region:

51,200 bytes

(12,800 words)

block region:

51,200 bytes

(12,800 words)

total:

l22,880 bytes

(30,720 words)

On a computer with a small address space. such as the PDP­ll. the initial sizes are

static region:

4,096 bytes

(2,048 words)

string region:

10,240 bytes

(5,120 words)

block region:

10,240 bytes

(5,120 words)

total:

24,576 bytes

(12,288 words)

143

The user may establish different initial sizes prior to program execution by using environment variables. As indicated previously, the sizes of these regions are increased at run time if necessary, but there is no provision for decreasing the size of a region once it has been established.

Free Space and the System Stack. On computers with system stacks that grow downward, such as the VAX, the system stack grows toward the allocated storage region. Between the two regions is a region of free space into which the allocated storage region may grow upward. Excessive recursion in C may cause collision of the system stack and the allocated storage region. This is an unrecoverable condition, and the result is termination of program execution. Similarly, more space may be needed for allocated storage than is available. This also results in termination of program execution. In practice, the actual situation depends to a large extent on the size of the user address space, which is the total amount of memory that is available for all the regions shown previously. On a computer with a small user address space, such as the PDP­11, the amount of memory available for allocated storage is a limiting factor for some programs. Furthermore, collision of the allocated storage region and the system stack is a serious problem. On a computer that supports a large virtual memory, the size of the system stack is deliberately limited, since the the total amount of memory available is so large that runaway recursion would consume enormous resources before a collision occurred between the system stack and the allocated storage region.

11.2 Allocation

Storage allocation in Icon is designed to be fast and simple. Garbage collection is somewhat more complicated as a result. Part of the rationale for this approach is that most Icon programs do a considerable amount of allocation, but many programs never do a garbage collection. Hence, programs that do not garbage collect are not penalized by a strategy that makes garbage collection more efficient at the expense of making allocation less efficient. The other rationale for this approach is that the storage requirements of Icon do not readily lend themselves to more complex allocation strategies.

11.2.1 The Static Region

Data allocated in the static region is never moved, although it may be freed for reuse. Co­ expression blocks are allocated in the static region, since their C stacks contain internal pointers that depend on both the computer and the C compiler and hence are difficult to relocate to another place in memory. Furthermore, since co­expression blocks are all the same size, it is economical and simple to free and reuse their space.

The C library routines malloc and free are used to allocate and free co­expression blocks in the static region. These routines maintain a list of blocks of free space. The routine malloc finds a block of the requested size, dividing a larger block if necessary, and revises the free list accordingly. The routine free returns the freed space to the free list, coalescing it with adjacent free blocks if possible. See Kernighan and Ritchie 1978 for a discussion of free­list allocation.

Icon contains its own version of these routines to assure that space is allocated in its own static region and to allow its overall memory region to be expanded without conflict with other users of malloc. Thus, if a user extension to Icon or the operating system calls malloc, Icon's own routine handles the request.

144

This means that the static region may contain space allocated for data other than co­ expression blocks, although this normally is not the case.

11.2.2 Blocks

For other kinds of blocks, Icon takes advantage of the fact that its own data can be relocated if necessary and uses a very simple allocation technique. The allocated region for blocks is divided into two parts:

When there is a request for a block of n bytes, the free pointer, blkfree, is incremented by n and the previous value of the free pointer is returned as the location of the newly allocated block. This process is fast and free of the complexities of the free­list approach.

Note that this technique really amounts to a free list with only one block. The problem of reclaiming fragmented space on the free list is exchanged for the process of reclaiming unused blocks and rearranging the block region so that all the free space is in one contiguous portion of the block region. This is done during garbage collection.

11.2.3 Strings

There is even less justification for a free­list approach for allocating strings. A newly created string may be one character long or it may be thousands of characters long. Furthermore, while there is space in blocks that can be used to link together free storage, there is no such space in strings, and a free list would involve additional storage.

Instead, the string region is allocated in the same way that the block region is allocated:

As with the block region, a garbage collection is performed if there is not enough space in the string region to satisfy an allocation request.

Соседние файлы в предмете Электротехника