April 90 - THE SECRET LIFE OF THE MEMORY MANAGER
THE SECRET LIFE OF THE MEMORY MANAGER
RICHARD CLARK
The Macintosh Memory Manager has changed in some subtle ways since it was
documented in Inside Macintosh . This, combined with the difficulty of observing what
the Memory Manager actually does, has led to a general misunderstanding of how the
Memory Manager works. This article first discusses some common myths about the
Memory Manager, then describes some ways to avoid memory-related errors and
control fragmentation without sacrificing execution speed.
Few parts of the Macintosh operating system raise as many questions as the Memory
Manager. Since the contents of RAM change dynamically, it's hard to really examine the
Memory Manager's behavior. This, combined with the unusual concept of relocatable
blocks and the fact that the Memory Manager is used by most of the operating system,
has left many Macintosh programmers confused about the behavior of the Memory
Manager and, more important, about the impact of this behavior on their applications.
MYTHS ABOUT THE MEMORY MANAGER
Several myths have grown up around the Memory Manager, serving to increase the
confusion about its real behavior. Three of the most prevalent--but mistaken--
beliefs are that (1) the Memory Manager will move and delete blocks, and otherwise
mangle the heap, at random; (2) using nonrelocatable blocks will cause serious heap
fragmentation; and (3) if you use Virtual Memory you don't need to worry about the
Memory Manager. We'll demolish each of these myths in turn.
MYTH 1: THE MEMORY MANAGER WILL MOVE AND DELETE BLOCKS, AND
OTHERWISE MANGLE THE HEAP, AT RANDOM
This simply isn't so. The Memory Manager is in fact quite predictable. It only moves
blocks under these circumstances:
• When your application calls a routine that allocates new blocks or
enlarges existing ones, when you request that blocks be moved, or when your
application calls a routine that in turn calls a ROM routine that may trigger
block relocation. Appendix A of the Inside Macintosh XRef lists all routines
defined in Inside Macintosh that may cause blocks to move.
• When the called routine is in a different segment from the code that
makes the call, or when the called routine is in the same segment as the
caller, but the called routine calls a routine or routines in a different
segment. If a called routine lies in a different code segment, the Segment
Loader may need to call the code segment in from disk and/or move it to the
top of the heap. Either of these actions can cause blocks to move.
MYTH 2: USING NONRELOCATABLE BLOCKS WILL CAUSE SERIOUS
MEMORY FRAGMENTATION
This is a half-truth at best. The Memory Manager actually does a good job of allocating
nonrelocat- able blocks, but can fragment the heap when these blocks are deallocated
and new ones allocated. Similar problems can happen when you start locking
relocatable blocks.
This myth actually has a basis in reality, as the earliest versions of the Memory
Manager did a poor job of allocating nonrelocatable blocks. Before the 128K ROMs
(introduced with the Macintosh 512Ke and Macintosh Plus), the Memory Manager
would not move a relocatable block around a nonrelocatable block in its quest to
allocate a new nonrelocatable block. This made the heap into a patchwork of relocatable
and nonrelocatable blocks, and caused general fragmentation problems, as illustrated
in Figure 1.
Figure 1. Fragmentation of Free Space
But that has long since changed, as NewPtr will now move a relocatable block around a nonrelocatable block when allocating memory. This tends to partition the heap into two
active areas, with all of the nonrelocatable blocks at the bottom of the heap, and the
relocatable blocks located immediately above. (See the sidebar "How the Memory
Manager Allocates Heap Space" for further details.)
On the other hand, for all of the improvements in allocation of nonrelocatable blocks,
there is still a problem withde allocation of these blocks. Since the Memory Manager
uses a "find the first free block that fulfills the request" strategy (as opposed to "find
a block that fits the request exactly"), if you allocate a subsequent block that is
smaller than the block you just deleted, the heap will become fragmented and the
amount of usable memory will likely decrease, as illustrated in Figure 3.
Figure 3. The Effect of Deallocating and Reallocating a Nonrelocatable
Block
Locking too many relocatable blocks can cause the same kind of fragmentation
problems as deallocating and reallocating nonrelocatable blocks. A well-trained
programmer uses the callMoveHHito move a relocatable block to the top of the heap before locking it. This has the effect of partitioning the heap into four areas, as shown
in Figure 4. The idea of usingMoveHHi is to keep the contiguous free space as large as possible. However,MoveHHi will only move a block upward until it meets either a nonrelocatable block or a locked relocatable block. UnlikeNewPtr (andResrvMem),MoveHHi will not move a relocatable block around one that is not relocatable.
Even if you succeed in moving a relocatable block to the top of the heap, your problems
are far from over. Unlocking or deleting locked blocks can also cause fragmentation,
unless they are unlocked beginning with the lowest locked block. In the case illustrated
in Figure 4, unlocking and deleting blocks in the middle of the locked area has resulted
in heap fragmentation. The relocatable blocks thus trapped in the middle won't be
moved until the locked block below them is unlocked.
Figure 4. The Effect of Unlocking Locked Blocks
MYTH 3: IF YOU USE VIRTUAL MEMORY, YOU DON'T NEED TO WORRY
ABOUT THE MEMORY MANAGER
Many people believe that the wide availability of Virtual Memory will remove the need
for careful memory management. Wrong! The Virtual Memory system is based on a
series of "pages" of memory that can be swapped to and from the disk, rather than on
individual blocks of memory. If you fragment RAM, you also "fragment" the contents of
the swap file and gain nothing. In fact, Virtual Memory makes careful memory
management even more critical, for two reasons. First, fragmenting the swap file will
degrade system performance worse than fragmenting physical memory will, since disk
access speeds are obviously slower than the RAM access speed. Second, the combination
of Virtual Memory and MultiFinder encourages users to run more programs at the
same time than they used to, and users often reduce the partition sizes of their
applications to squeeze in "one more program.
THE EXPERT'S GUIDE TO MEMORY MANAGEMENT
Now you know that the Memory Manager moves blocks of memory only at certain
well-defined times; that nonrelocatable blocks can be allocated without causing serious
fragmentation in the heap, although deallocation and reallocation of these blocks, and
locking too many relocatable blocks, can cause problems; and that use of Virtual
Memory makes careful memory management even more important. It's time to put this
knowledge into action. In this section, you'll learn how you can work cooperatively
with the Memory Manager to increase the efficiency and robustness of your
applications.
TO AVOID DANGLING POINTERS
As every programmer learns early on, the gravest side effect of the Memory Manager's
penchant for moving blocks of memory is the peril of dangling pointers. (For a
refresher on how these come about, see the sidebar entitled "A Primer on Handles and
Their Pitfalls" in Curt Bianchi's article "Using Objects Safely in Object Pascal" in this
issue.) And the best defense against having to spend hours--or days--debugging
errors caused by dangling pointers is to anticipate situations in which block movement
might occur, and if it does occur, will throw a monkey wrench into the works. In these
situations, much grief can be saved by using a temporary local or global variable to
store a duplicate of the relocatable block. (Note, though, that this trick only works
properly if the block can stand on its own--that is, it's not part of a linked list.)
Some of the situations that might get you into trouble are well documented, such as the
use of the WITH statement in Pascal. Other dangerous situations are less obvious, so
we'll explore them here.
Be careful when evaluating expressions. There are times when evaluating a
seemingly innocent expression might have serious side effects. For example, look at
the following code:
TYPE
windowInfoHdl = ^windowInfoPtr;
windowInfoPtr = ^windowInfo;
windowInfo = RECORD
aControlHdl: ControlHandle;
aWindowPtr: WindowPtr;
END;
VAR
myHandle : windowInfoHdl;
BEGIN
myHandle := windowInfoHdl(NewHandle(sizeof(windowInfo)));
{ The next 2 statements have problems. }
myHandle^^.aWindowPtr := GetNewWindow(1000, NIL, WindowPtr(-1));
myHandle^^.aControlHdl :=
GetNewControl(1000, myHandle^^.aWindowPtr);
END;
In Pascal, the above statements would probably cause a run-time error. The problem
is in the expression myHandle^^.something :=" as the compiler evaluates
expressions from left to right and calculates the address on the left side of the
assignment statement before making the toolbox call. When GetNewWindow is called, myHandle^^ is moved (we passed in NII to force a call to NewPtr) and the address on the left- hand side is no longer valid! This means that the returned WindowPtrwill be written into the wrong area of memory, and the program will probably crash.
While both statements suffer from the same basic problem, the first one is more
likely to cause a crash than the second one and is therefore easier to debug. Why is
this?
nonrelocatable block at the bottom of the heap, forcing relocatable blocks upward in
the process. The other statement, containing GetNewControl, allocates a relocatable block, which usually appears above the existing blocks, with block movement
happening only if a compaction is required.
While this problem occurs most frequently in Pascal, C programs are not immune.
Most C compilers on the Macintosh evaluate the right- hand side of an assignment
before the left-hand side--which avoids this problem entirely--but the order of
evaluation is not guaranteed by the ANSI standard.
This problem can be solved easily by using a temporary variable. The following code
avoids the problem:
VAR
myHandle: windowInfoHdl;
aWindowPtr: WindowPtr; { This is allocated on the }
{ stack, so it won't move. }
aControlHandle: ControlHandle; { Also on the stack. }
BEGIN
myHandle := windowInfoHdl(NewHandle(sizeof(windowInfo)));
{ Copy the result into a temporary variable, then copy }
{ that into the relocatable block. }
aWindowPtr := GetNewWindow(1000, NIL, WindowPtr(-1));
myHandle^^.aWindowPtr := aWindowPtr;
aControlHandle := GetNewControl(1000, aWindowPtr);
myHandle^^.aControlHdl := aControlHandle;
END;
Be careful when using callback routines. When you pass pointers to your
routines, say as a ROM callback routine, and your routines are in multiple segments,
you need to be careful.
The following code is fine now, but we'll soon edit it to demonstrate the problem:
PROCEDURE MyCallback(ctl: ControlHandle; part: INTEGER);
{ This represents a callback routine used for continuous }
{ tracking in controls. }
BEGIN
{ Do whatever you need to do. }
END;
PROCEDURE HandleMyControl(theControl: ControlHandle;
pt: Point);
BEGIN
part := TrackControl(theControl, pt, @MyCallback);
END;
The expression @MyCallbackpushes the address of the callback routine onto the stack
before calling TrackControl. If the two routines are in the same segment, as in the preceding example, all is fine. The segment is locked in memory when @MyCallback is
both evaluated and used; therefore, the address is valid. If the two routines are in
different segments, this also works, as the compiler takes the address of the jump
table entry for MyCallback.
In some cases, and especially in C, you may choose to set up a table of procedure
addresses. But if you store the address of the routine into a variable, strange things
may happen. Take a look at the following code:
{ ----------------------------- }
{ For an example, we'll place the addresses of two control }
{ tracking routines into an array, then use them. }
VAR
gCallbackArray: ARRAY [1..2] OF ProcPtr;
{ ----------------------------- }
{$S Segment1 }
PROCEDURE MyVScrollCallback(theControl: ControlHandle;
part: INTEGER);
BEGIN
{ This will get called if our control is a vertical }
{ scrollbar. }
END;
PROCEDURE MyVScrollCallback(theControl: ControlHandle;
part: INTEGER);
BEGIN
{ This will get called if our control is a horizontal }
{ scrollbar. }
END;
PROCEDURE InitCallbackArray;
{ Fill in the addresses in the global “Callback” array. }
BEGIN
{ Problem: Since we're in the same segment, these aren't }
{ addresses of the jump table entries, but are absolute }
{ locations in RAM! If the segment moves (i.e., if }
{ UnloadSeg is called), the addresses will be invalid. }
gCallbackArray[1] := @MyVScrollCallback;
gCallbackArray[2] := @MyHScrollCallback;
END;
END.
{ ----------------------------- }
{$S Main }
PROCEDURE HandleAScrollbar(theControl: ControlHandle;
pt: Point);
{ We'll call this if the user clicks in our scrollbar (except }
{ if she clicks in the thumb, which uses a different kind of }