Jump to content
IGNORED

Program memory management in Pascal


apersson850

Recommended Posts

In another post I described how simple it is to make more efficient use of the small memory available in a standard 99/4A. That post described the handling of data.

Now let's look at the program itself.

 

The code pool concept

When the p-system is running, it uses all 48 K RAM (32 K CPU RAM and 16 K VDP RAM) for different purposes.

VDP RAM is of course used to create the image to display, with character pattern table, display table etc. The remaining part of the VDP memory is used as the primary code pool. That's near 12 Kbytes or so.

 

In the expansion memory, the 8 K part is used solely by the system. Here are the code for the parts of the operating system that can't run from the p-code card, system variables, the 80 column screen and such things. Yes, the p-system normally runs in text mode, 40 columns wide, but by default it simulates an 80 column screen, where you can move the viewport to the right or left, so you can see one half of the screen at each time. Thus by simply writing to the screen, you get these 80 columns simulated with no effort in your own program.

 

The 24 K RAM part is used as the secondary code pool. Code is loaded here if the primary code pool is full, or if the code segment contains TMS 9900 machine code (assembly programs), as they can't execute from the video memory. This part of the memory is also used for the stack. In the post about the data memory I described some use of the stack for data memory allocation. The stack is also used for calulations, tracing subprogram calls and such things. The stack starts at the highest memory location (FFFFH) and grows downwards.

At the other end of the 24 K RAM area, the heap is located. The heap is similar to the stack in that it grows and shrinks on demand, but it's different in how it operates. On the stack, new data is allocated on top of what's already there. When data is discarded, it's always removed from the top.

On the heap, on the other hand, data is of course originally allocated in the same way. If you allocate ten items, they are added to the heap in that order. The only difference then is that it grows in the other direction, as it starts at A000H and grows upwards. The big difference is that you can remove things from the heap in any order. When you remove something that was created early, a "hole" will appear in the heap. The p-system IV.0 does implement a true heap, not just another stack that's called a heap, like some earlier p-systems did. Thus a hole can be reused, provided the new variable to allocate isn't larger than it can fit in the hole. The p-system can also garbage collect the heap, so it can be packed when needed.

The alternate code pool resides in between the heap and the stack. The space available for it hence depends on what's going on in the system.

 

Assuming your program fits in the primary code pool, it will not compete with the stack and heap for space in the 24 K memory area. But if it's larger than what fits in the primary code pool, or if it contains assembly support, then it must be loaded in the secondary pool. In the simplest of these cases, the primary pool is completely unused.

That's of course not good. Now we should be aware that the operating system itself may load code in the code pools too, if it can't run that code from the p-code card directly. Fortunately, the PME (p-machine interpreter, the assembly code that executes the p-code) is flexible enough to be able to run p-code from CPU RAM, VDP RAM or even directly from GROM. The p-code on the card itself is stored in GROM chips, so in many cases it can run directly from there. That's the reason for why the 48 K TI 99/4A could run the p-system IV.0, where the Apple II, which also had 48 K RAM, but no p-code card with separate GROM, required an additional 16 K RAM card to be able to run the simpler first versions of the p-system, called Apple Pascal.

 

Code segmentation

But how do we make a larger program, that doesn't fit as it is in the primary code pool, to still fit in there, so we can use as much data memory as possible? The p-system's solution to this is called segments.

In the p-system, a code segment is a piece of code that must be loaded into memory as a consecutive unit. When there are more than one code pool available, as in the 99/4A, it's an advantage if the largest segment you need to load is smaller than the smallest code pool available, since then the system can always place it where it's most efficient.

 

In Pascal, you can create code segments in your own program, when there's is a benefit to do so. A classic reason is that you have code that runs to set things up before your main program really starts to run, but then never runs again. Or the opposite, code that runs only when the main program is about to stop, to save results in files or whatever.

Another typical example can be code that runs only when you are printing results, or when you are inputting data.

 

Have a look at this program. It's a stub that contains a number of different such functions.

program doesitall1;

type
  alldata = record
    (* All things needed *)
  end;

var
  bigdata: alldata;
  level: integer;

procedure initialize;

begin
  (* Fixes all setup in the bigdata structure *)
end;

procedure cleanup;

begin
  (* Saves what need to be saved in the bigdata structure *)
end;

function menu: integer;

var
  temp: integer;

begin
  repeat
    write('What to do? ');
    read(temp);
  until temp in [1..4];
  menu := temp;
end;

procedure dataentry;

begin
  (* Handles user input to the bigdata structure *)
end;

procedure calculate(complexity: integer);

begin
  for i := 1 to complexity do
  (* Makes some complex calculations on bigdata *);
end;

procedure printservice;

begin
  (* generates hardcopy of the things in bigdata *)
end;

being (* doesitall1 *)
  initialize;
  level := 3;
  repeat
    case menu of 
      1: dataentry;
      2: calculate(level);
      3: printservice;
    end;
  until menu=4;
  cleanup;
end.

When the main program starts, it begins by a setup procedure. Then it repeatedly calls menu to get the user's choice for what to do. The choice calls a corresponding procedure, until the user quits, in which case a cleanup is done before exiting.

In a program like this, everything is loaded as one code segment. If it doesn't fit in the smaller main code pool, it has to compete with stack and heap space in the secondary code pool.

Now, assuming it's big enough so that is a problem, how can we handle that? In many systems, like in Extended BASIC, it's possible to run one program from another, so you can overlay code in memory. But data is usually lost when you do it, so it has to be saved manually. In assembly, you can do everything, but that also means that simple things like the menu function above become complex. Forth has other means available, like forgetting definitions and loading other defintions from disk. But you have to manually control that, or define a subsystem that does.

 

If you use Pascal with the p-system, the only thing you need to do is to consider which routines you don't always need. Here, it's pretty simple to see that this list of procedures have one thing in common: They are never needed simultaneously.

  • Initalize
  • Cleanup
  • Dataentry
  • Calculate
  • Printservice

Once we have figured that out, we only need to inform the system about it. You do that by telling the compiler which parts of the program can be separate segments in memory.

 

program doesitall2;

type
  alldata = record
    (* All things needed *)
  end;

var
  bigdata: alldata;
  level: integer;

segment procedure initialize;

begin
  (* Fixes all setup in the bigdata structure *)
end;

segment procedure cleanup;

begin
  (* Saves what need to be saved in the bigdata structure *)
end;

function menu: integer;

var
  temp: integer;

begin
  repeat
    write('What to do? ');
    read(temp);
  until temp in [1..4];
  menu := temp;
end;

segment procedure dataentry;

begin
  (* Handles user input to the bigdata structure *)
end;

segment procedure calculate(complexity: integer);

begin
  for i := 1 to complexity do
  (* Makes some complex calculations on bigdata *);
end;

segment procedure printservice;

begin
  (* generates hardcopy of the things in bigdata *)
end;

being (* doesitall2 *)
  initialize;
  level := 4;
  repeat
    case menu of 
      1: dataentry;
      2: calculate(level);
      3: printservice;
    end;
  until menu=4;
  cleanup;
end.

This is all it takes from you. The system will now load only the non-segment parts of the program doesitall2 when you execute it. Immediately, there will be a cross-segment call to initialize. But as a user, you need to do nothing. The operating system will check if segment initialize is in memory. It's not the first time, so it will automatically fetch it from disk and load into an available space in code pool.

Then, as you enter your different choices from the menu system, the appropriate cross-segment calls will be made, and the system will load the code as needed. If you first run data entry, then maybe it fits at the same time as initialize remains in memory. If you then do a printout, that part perhaps also fits together with the already loaded segments. When examining the printout, you realize you should return to data entry. In this case, no loading of any segment is done, since the system finds that it's still there, since you used it last time.

Now you call calculate. The segment fault (segment is not in memory), which triggers loading the segment, in this case causes a memory fault. The primay code pool is full with other segments, and there's not enough space between the code and the stack in the secondary pool to load the new segment.

In this case, the system first attempts to free up more space in the code pool by optimizing the position of the already loaded code. P-code is dynamiclly relocatable, so the system will move code segments around, to make sure they are adjacent, so they occupy a minimal amount of memory. Then it will try to load the missing code segment again. If this faults once again, it will examine the list of currently loaded segments. The system keeps track of whether there is any open call to a segment. If so, it must remain in memory. This means that if one segment calls another, both must be in memory at the same time.

But in this case, there are segments that aren't called. Like initialize. The system now checks the segments that are possible to remove. It keeps a simple call history, so the one that hasn't been used for the longest time is sacrificed. In this case, it will be initialize.

Eventually, you select to terminate the program, in which case the cleanup segment is loaded. One or more other segments are then removed from memory, if it's necessary to make the cleanup segment fit.

 

As you now hopefully understand, the p-system provides you with quite a complex (at least for its time) system for memory management. You can also see that using this powerful tool is comparatively simple, or even very simple, in your program.

 

The next post on a similar topic will be about separate compilation. It's a very powerful tool, which combines all the memory management features I've described in the first two posts with an ability to write code that's both easier to manage, reuse and debug, in a format where you get all these benefits with very little extra work.

There is a reason I've always claimed that the p-system is/was the most comprehensive software development environment for the TI 99/4A!

Edited by apersson850
  • Like 7
Link to comment
Share on other sites

In another thread, @TheBF asked about the amount of virtual memory in the TI with the UCSD p-system.

 

First, the implementation doesn't support virtual memory in the true meaning. If you need a data structure larger than what's available, you're out of luck. The system will crash with a stack overflow if you try to run such a program.

But for the code, it's a different thing. As you can read above, the p-system can use automatic overlays, to be able to execute programs that are larger than what can fit in memory at one single time.

So how much memory is this equivalent to?

There's no simple answer to that question. In theory, you could optimize segment sizes to make full use of everything, and then it will be almost unbelievable large for this small machine. In reality, segment sizes in programs vary quite a lot. It's not like you can program along and just insert a new segment when code size says so. You have to group code in segments in a logical way, so that things that depend on each other are in the same segment. Remember that when you call a procedure in a different segment, there are at least two in memory. The segment you call from and the segment you call to. If the second segment always need something in a third one, there's nothing to gain to make it a third segment, as in that case the original segment, the first called and the second called segments have to be in memory at the same time.

 

This leads to that code belonging to a segment has some kind of logical relation. As in the above examples, it could be initialization or some specific operation on data. Like sorting or reporting.

If you look at existing code, it's usually so that segments are somewhere between 100 and 5000 words, i.e. from a fraction of a kilobyte to 10 Kbytes. Accounting for the size of the code pools in the 99/4A, it doesn't make sense to make segments larger than 10 Kbytes.

 

If we now assume that you are good at grouping functions into reasonable segments, then you may still end up with an average size of the segments of no more than 5 Kbytes. But considering that the p-system IV.0 lifted the limit for max number of segments from 16 to a whopping 255, we are talking about a code size of 1.2 Megabytes! With the option to double that, if segment sizes can be really optimized. But such an optimization would require a lot of manual work in planning the code, so then you couldn't say that it's easy to use.

 

Still, a code size of 1.2 Megabytes on a 48 Kilobyte computer must be considered impressive, especially as we are talking technology from 1980. Don't forget that we are talking about compiled code size here, not the source coude. With the inherent compactness of the p-code, the size of the source code would be several times that of the executable code. It's easy to understand that even with just a minimum of planning, the limit for the program size the TI 99/4A can run, when executing Pascal programs under the p-system, was limited by the available space on floppies rather than by internal memory in the machine.

 

Come this far, you may suspect that the p-system would incorporate means to handle both the burden of compiling such a large program, as well as being able to cope with code files larger than the space available on the diskettes (especially the first 90 Kbyte disks). And you would be right, since right off the bat, it can do both. But the capabilities there are so extensive that I'll save that for yet another thread about code management in the p-system.

  • Like 7
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...