The memory models were defined by compilers for high-level languages, and were reasonably standard between Microsoft, Borland and Watcom. The Small, Medium, Compact and Large models appear to have originated with an Intel compiler from 1980.
First, a brief explanation of how the 8086 architecture worked. It was a 16-bit CPU that could only address memory in chunks called segments. Each segment was 65,536 bytes in size, because that was the number of bytes 16 bits could address. A program could use four segment registers at a time, SS (stack), CS (code), DS (data), and ES (extra). A 16-bit pointer within one of these segments was a near
pointer. Originally, these segments could start at any 16-byte “paragraph” of the one-megabyte “conventional memory,” so a far
pointer needed 32 bits to hold a 20-bit addres. Later machines added the ability to switch between segments of “expanded” or “extended” memory, to protect memory as not writable or not executable, as well as adding two more segment registers, FS (doesn’t stand for) and GS (anything).
I wrote a long answer a while back about the reasons Intel made this choice. It made sense at the time, but only because the engineers believed that Intel would someday be able to break backward compatibility with it and move on.
Memory models defined whether a program would assume all its code was in a single segment, all its data, both or neither. This determined whether the program could assume any arbitrary function it called was in the same code segment, or any data it accessed was already in its data segment, and therefore whether it needed extra memory to store the segment and extra code to update the segment register. Assembly language didn’t really need a formal memory model, as the programmer could always decide whether to write a near
or a far
instruction. High-level languages, though, needed to make a trade-off between using the smaller near
pointers, which were more efficient, and the wider far
pointers, which could support more than 64K of code or data. (Similarly, programmers today sometimes write 32-bit code on a 64-bit machine because 32-bit pointers use less memory.) The terminology that became standard was:
- The Small memory model had one segment for all the code, and another for all the data. All pointers were, by default,
near
pointers.
- The Compact model had no more than 64K of code, so all jumps and calls could be
near
, but could deal with more than 64K of data. In particular, it could give the stack its own segment, and be at less risk of a stack overflow. Jumps, calls and function pointers in languages that had them were all near
, but pointers to data were far
by default. This was probably the most commonly-used model on MS-DOS.
- The Medium model had no more than 64K of data, and more than 64K but less than 640K of code. (MS-DOS was not able to load code above address
0xA0000
, at least not the normal way, because that was where IBM had decided to put the video memory on the original PC.)
- The Large model used
far
pointers by default, and could support more than 64K of both code and data.
Importantly, although the Compact or Large models supported more than 64K of data, no individual array, structure or object was allowed to be larger than 64K. Each such object needed to fit within a single segment. (This is also why C and C++ still do not allow you to compare or subtract two pointers from separate objects. This would break on an architecture that uses segments.)
A program using a larger model might still be able to use near
pointers locally, or place a family of functions into the same segment group where they could call each other with near
calls. One with a smaller memory model might have a few far
functions outside the main code segment, or only a few pieces of far
data, and fit the rest under the 64K limit.
There were a few other memory models as well.
- Borland Turbo C, and a few other compilers, supported a Tiny memory model, where all code and data fit into a single 64K segment.
This existed for historical reasons. Intel had based its 8086 on an earlier CPU, the Intel 8080. The 8080 only supported 64K of memory and 16-bit addresses, without segments. There were a lot of programs written for it, and in particular, the circumstances of MS-DOS’ creation (a fascinating story which anyone reading this far down the page on a retrocomputing site already has heard some version of) meant that MS-DOS 1.0 supported a .COM
format for executables based on CP/M for the 8080. The primary use of the Tiny model was that a program that used it could be compiled to a smaller .COM
executable, rather than the .EXE
format.
Later on, compilers added a sixth model.
- The Huge model used
far
pointers for code and data, but treated data as a flat address space. The difference between Large and Huge was that, if pointer arithmetic on a far
data pointer overflowed the bottom 16 bits, they would wrap around. If pointer arithmetic on a huge
data pointer overflowed the bottom 16 bits, it would increment the segment.
This had the minor benefit that two data pointers were aliases of each other if and only if they were encoded with the same bits, and the much more important advantage that arrays and structures were now allowed to be more than 64K in size.
Finally, some MS-DOS programs (most but not all of them, games) in the ’90s began using DOS extenders. Many of these used undocumented tricks to let a DOS program use a flat, 32-bit memory space. Toward the end of DOS’ lifespan, these became standardized as DPMI and other interfaces.
Does Java have undefined behaviour?
states that incrementing a signed int in C is one example of UB, it shouldn't preclude someone askingWhy does my C variable get weird values when it gets too big?
. I don't think anyone is going to think the first question will relate to the second, so they won't even look at the answers. But, in any case, thanks for finding the meta question, I'll move further comment over to there.