CS360 Lecture notes -- Fragmentation

  • Jim Plank
  • Directory: /blugreen/homes/plank/cs360/notes/Fragmentation
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Fragmentation/lecture.html


    Fragmentation is a concept that pops up in a lot of places in systems programming. Fragmentation occurs when a user program has allocated memory, but doesn't use it. An example is in the heap from Malloc lecture #2, when malloc() has 8130 bytes in its free list. This is memory that the program has allocated from the operating system, but is not using.

    Were the machine to have unlimited memory, fragmentation wouldn't be a problem, but most machines have on the order of 16-100 megabytes of memory, and perform multiprogramming. In other words, there are many processes running on the machine at one time, all of which use memory. This means that the processes may compete for memory, and thus fragmentation is bad, as it is a waste of memory. In general, we want to minimize fragmentation.

    Fragmentation is often reported in terms of the percentage of allocated memory that is wasted. For example, suppose a program has a code segment of 4K, a globals segment of 1K, a heap of 8K and a stack of 1K. Moreover, suppose that malloc() has 6K on its free list. Then the fragmentation of the program is 6/(4+1+8+1) = 43%. This isn't too bad since the total memory usage of the program is 14K. Were that 14M, or were there 1000 such programs running on the machine at the same time, the fragmentation would be very wasteful.

    There are two types of fragmentation -- external and internal. External is fragmentation due to unused memory on nodes of the free list. In the example above, the 6K of unused memory is external fragmentation.

    Internal fragmentation is a little more subtle. It comes about as memory that is allocated for infrastructure, and not for the user. For example, the 8 bytes that malloc() always allocates for bookkeeping contributes to internal fragmentation. To see why this is, suppose you have the following piece of code:

    char *array[100000];
      int i;
      for (i = 0; i < 100000; i++) array[i] = (char *) malloc(8);

    The programmer may expect this code to allocate 800K of 8-byte quantities. However, since 8 extra bytes are always allocated for bookkeeping, 1.6M are allocated instead. This is internal fragmentation -- 50% of the heap is really infrastructure, and may be considered as waste from the programmer's point of view.

    Another example of internal fragmentation is in the example above where the user called malloc(8) and we returned a pointer to 16 bytes because we had a chunk of 24 bytes in the free list, which couldn't be broken into two pieces. This is fragmentation because this memory is really wasted.

    The difference between internal and external fragmentation is kind of subtle, but you should see that there is a distinction.

    Combatting internal fragmentation

    One way to combat internal fragmentation is to keep multiple free lists. For example, you can have a free list for 4-byte quantities, one for 8-byte quantities, one for 16-byte quantities, and one for any size greater than 16. When you allocate from the 4, 8, or 16 byte freelist you don't need to store the size of the chunk for free(), and thus need have no internal fragmentation. For the larger malloc()'s internal fragmentation isn't as much of an issue. This isn't the whole story (if you think about it a little, you may have some questions concerning how free() works), but I'll leave it in this sketchy form. Some systems do allocate memory in just this way. Our version of Unix does not.

    Freelist allocation algorithms

    Suppose your free list looks as follows:
    head  ---->  Chunk 1 ----> Chunk 2 ----> Chunk 3 ----> Chunk 4 ----> Chunk 5.
                 40 bytes      24 bytes      72 bytes      16 bytes      8K bytes

    If you get a malloc(8) request you can allocate the necessary 16 bytes (8 for the user and 8 for bookkeeping) from any of these chunks. Which one should you choose? There are a few standard algorithms for doing this. They are as follows:

    1. Best fit -- find the chunk that gives the closest fit and choose that one.
    2. Worst fit -- always allocate from the largest chunk
    3. First fit -- start from the head, and allocate the first chunk that is large enough
    4. Next fit -- start at the chunk after the one just allocated, and pick the first one that is large enough (treat the free list as if it's circular).
    Thus, if you received three requests malloc(60), malloc(32), malloc(8), then the chunks allocated would be as follows:
    1. Best fit: malloc(60): chunk 3, malloc(32): chunk 1, malloc(8): chunk 4,
    2. Worst fit: malloc(60): chunk 5, malloc(32): chunk 5 (from the remainder), malloc(8): chunk 5 (from the remainder),
    3. First fit: malloc(60): chunk 3, malloc(32): chunk 1, malloc(8): chunk 2,
    4. Next fit: malloc(60): chunk 3, malloc(32): chunk 5, malloc(8): chunk 5 (from the remainder), or chunk 1, depending on how you treat the remainder (i.e. if the "next" chunk is the remainder itself, or the chunk after the remainder).

    As it turns out first fit / next fit tend to give the best performance -- best fit results in a lot of internal and external fragmentation, and worst fit is even worse. Both methods require you to search the entire freelist at each malloc() (or, at least perform O(log n) list actions if you keep the freelist in a heap) which is obviously going to give worse performance than first or next fit.

    You may implement any allocation algorithm you want in your malloc().