Memory, Heap, Stack, Loader

This page was translated by a robot.

When executing a program programmed in C or C++, a so-called process is started by the system. In modern computer systems, each process is assigned its own generous memory space, which is divided into several segments. On the one hand, these segments store static and dynamic data, but on the other hand they also contain the executable machine code and the runtime system. Most of the storage space nowadays is that for the dynamic data. It is called the heap and is generally freely available to the process (with some restrictions).

The processing of the machine code is taken over by one or more threads (in German: threads) in each process. When a process is started, there is usually only one thread, but a programmer can start new, parallel threads by calling certain library functions. Each thread operates on the same heap made available to the process, but the individual threads are independent execution units and must therefore have a data structure specially reserved for the respective thread in which they can store local data in order to run independently. This data structure is called a stack .

A so-called loader is responsible for setting up all of these memory structures and for correctly initializing the various memory segments and the runtime system .

Details

The heap describes the total memory available to a process. On modern 32-bit systems, each individual process can access up to 4 gigabytes of memory, on 64-bit systems even a billion times more. However, certain parts of the heap are reserved for the runtime system , which guarantees that the program runs safely and that the data is stored without collisions.

A part of the reserved heap contains the executable machine program, which is copied from the hard disk to memory by a so-called loader before the process is started, so that it is always available during execution. This part was formerly known as the so-called text segment . After loading the text segment, the loader reserves and initializes space for the global variables. Before the loader hands over control to the actual process, it also reserves and initializes the necessary data structures for the runtime system, including the stack for the first (main) thread.

The size of a stack varies depending on the system and architecture and can even be specified manually by the programmer. By default, stacks today are a few tens of kilobytes to a few megabytes in size.

During the execution of a thread, functions are continually called or returned from functions. Each called function must therefore store information about the state of the processor before the function was called. On the one hand, this includes the data registers, but also, for example, the address of the next machine instruction to be executed after the return, i.e. the return address. In addition, each function can store local variables and also return a value to the parent function. Since all function calls are structured hierarchically, a stack, which is also referred to as the call stack , is suitable for storing and organizing all this data.

The current position within the stack is given by a so-called stack pointer , for which there is often a specially reserved processor register. To put it simply, when a function is called, the stack pointer is shifted within the stack in such a way that there is enough space for all status data and local variables. When returning from a function, the data stored at these points is copied back into the register provided and the stack pointer is moved back to the previous address. A somewhat more detailed version can be found in the call stack .

Allocations

Allocation means reserving a certain amount of bytes within the total available storage space. Allocations can take place both in the heap and on the stack, with some allocations being made implicitly by the language itself and others having to be explicitly programmed out. With allocations, a distinction must be made between data blocks and variables.

Data Blocks

Data blocks can only be allocated on the heap. A programmer must explicitly request the desired memory space using the new operator or by calling the malloc function . These methods, given by the C and C++ languages, use the runtime system to find and reserve a free data block of the desired size in the heap. The runtime system checks whether there is enough space and ensures that no two reserved blocks overlap. Furthermore, previously uninitialized blocks are overwritten with zeros, so that no non-process data is available. A non-negligible amount of time is therefore required for each allocation.

In order to release the memory again (de-allocation), a programmer must explicitly use the delete operator or the free function . The runtime system also needs a certain amount of time for deallocation.

Note: Both the new operator and the malloc function can be used for any data and respective data blocks can coexist in the same process without any problems. However, the runtime system distinguishes between the two methods, so newblocks allocated with can generally not be freedeallocated with the function and vice versa.

Variables

A definition of a variable causes data of the specified type to be provided. It should be noted that a variable can describe not only a base type, but also, for example, an entire array. A single variable can therefore be of any size. Where the storage space required for the variable is allocated is determined by the storage class of the variable.

Only variables of the autostorage class are stored on the stack. This storage class is the standard class for variables that are defined within functions, so variable definitions within a function without specifying a storage class automatically belong to the autostorage class. Since such variables are only valid for the current function, they are also referred to as local variables.

autoThe programmer does not have to worry about the allocation or deallocation of variables in the storage class, hence the name: automatic allocation and deallocation. The allocation and de-allocation of enough memory is done automatically by moving the stack pointer. The advantage here is that both allocation and deallocation do not require any time.

Storage class variables are stored in the heap static. The memory space is already allocated during the loading process of the program, with the loader automatically making enough memory available before the start of the process.

If a variable belongs to the registerstorage class , it is not stored in either the heap or the stack. The registerstorage class causes a variable to be stored in a processor register. If this is not possible (for various reasons), the variable is automatically moved to the autostorage class.

If the externstorage class is specified, the allocation and deallocation takes place explicitly at a different point. In this case, the specification of the variable serves purely as a declaration.

The mutablestorage class only appears within the declarations of classes. Since this is a declaration and not a definition, the storage location is only determined when a corresponding object is instantiated. Thus, this storage class has nothing to do with the storage location in the actual sense, but only with the access authorization. The typedefstorage class is not a real storage class and therefore does not allocate any data, but only defines a new type.

Detailed information can be found in the respective storage classes.

Impact on the Programming Style

The big advantage of the heap is its size. While the stack may only use a few kilobytes for variables, the heap can store gigabytes of data blocks. The big advantage of the stack, however, is that allocations do not require any time expenditure. Because of these advantages and disadvantages, a programmer should consider how and where allocations should take place.

Frequent calls of newand delete, respectively of mallocand freecan strongly influence the performance of a process. With each individual call, the runtime system requires a non-negligible amount of time to manage the heap. This can be countered by, for example, allocating frequently used objects to only one location and being able to address them if necessary and, if necessary, initialize them using an appropriate method. Another possibility is the simultaneous allocation of a large number of objects, which are then stored and managed in a suitable data structure (e.g. in an array, a list or a pool).

It should be noted that little to no time is required for the allocation of variables. Regardless of this, initializing variables or even calling a constructor still takes time. Aggregate assignments can also be very time-consuming and lead to a severe loss of performance if functions are called frequently. It is therefore advisable to avoid aggregate assignments and complex constructors within frequently called functions whenever possible.

Due to the speed advantage, it is often advisable to pack as much data as possible (e.g. entire arrays) in local or global variables. However, since local variables are stored on the stack, the size is very limited and, in the worst case, can even cause the process to crash, since not so many function calls can be executed due to the lack of space on the stack. In addition, if the stack is used too much, the caching structure provided by the processor can become useless, which leads to severe performance losses.

The use of large global variables always leads to a longer loading time. The loader must ensure that no non-process data is available, which is why it must initialize all global variables. If a programmer does not specify an initialization, the loader fills the variables with nothing but zeros.

It should be noted that modern compilers may perform automatic optimizations, such as local variables that are too large being automatically allocated in the heap by the runtime system. The decision as to which variable belongs in a registerstorage class is also handled differently depending on the compiler and architecture.

Next Chapter: Call-Stack