Dynamic Memory Allocation and Fragmentation in C and C
Dynamic memory allocation in C and C++ can cause issues for real-time embedded systems due to non-deterministic allocation times and memory fragmentation. Memory is divided into static, automatic, and dynamic heap spaces. Functions like malloc() and free() allocate and free memory but can cause fragmentation over time as memory usage changes. Fragmentation occurs when free memory is split into small non-contiguous chunks, preventing the allocation of larger contiguous blocks and leading to unexpected failures. Care must be taken to prevent memory leaks and ensure deterministic allocation performance for real-time systems.
Download as DOC, PDF, TXT or read online on Scribd
100%(1)100% found this document useful (1 vote)
385 views
Dynamic Memory Allocation and Fragmentation in C and C
Dynamic memory allocation in C and C++ can cause issues for real-time embedded systems due to non-deterministic allocation times and memory fragmentation. Memory is divided into static, automatic, and dynamic heap spaces. Functions like malloc() and free() allocate and free memory but can cause fragmentation over time as memory usage changes. Fragmentation occurs when free memory is split into small non-contiguous chunks, preventing the allocation of larger contiguous blocks and leading to unexpected failures. Care must be taken to prevent memory leaks and ensure deterministic allocation performance for real-time systems.
Download as DOC, PDF, TXT or read online on Scribd
You are on page 1/ 13
Dynamic Memory Allocation and Fragmentation in C and C++
Colin Walls, Mentor Graphics
Newbury UK Abstract: In C and C++, it can be very convenient to allocate and de-allocate blocks of memory as and when needed. This is certainly standard practice in both languages and almost unavoidable in C++. However, the handling of such dynamic memory can be problematic and inefcient. For desktop applications, where memory is freely available, these difculties can be ignored. For embedded - generally real time - applications, ignoring the issues is not an option. Dynamic memory allocation tends to be nondeterministic; the time taken to allocate memory may not be predictable and the memory pool may become fragmented, resulting in unexpected allocation failures. In this session the problems will be outlined in detail and an approach to deterministic dynamic memory allocation detailed. C/C++ Memory Spaces It may be useful to think in terms of data memory in C and C++ as being divided into three separate spaces: Static memory. This is where variables, which are defned outside of functions, are located. The keyword static does not generally afect where such variables are located; it specifes their scope to be local to the current module. Variables that are defned inside of a function, which are explicitly declared static, are also stored in static memory. Commonly, static memory is located at the beginning of the RAM area. The actual allocation of addresses to variables is performed by the embedded software development toolkit: a collaboration between the compiler and the linker. Normally, program sections are used to control placement, but more advanced techniques, like Fine Grain Allocation, give more control. Commonly, all the remaining memory, which is not used for static storage, is used to constitute the dynamic storage area, which accommodates the other two memory spaces. Automatic variables. Variables defned inside a function, which are not declared static, are automatic. There is a keyword to explicitly declare such a variable auto but it is almost never used. Automatic variables (and function parameters) are usually stored on the stack. The stack is normally located using the linker. The end of the dynamic storage area is typically used for the stack. Compiler optimizations may result in variables being stored in registers for part or all of their lifetimes; this may also be suggested by using the keyword register. The heap. The remainder of the dynamic storage area is commonly allocated to the heap, from which application programs may dynamically allocate memory, as required. Dynamic Memory in C In C, dynamic memory is allocated from the heap using some standard library functions. The two key dynamic memory functions are malloc() and free(). The malloc() function takes a single parameter, which is the size of the requested memory area in bytes. It returns a pointer to the allocated memory. If the allocation fails, it returns NULL. The prototype for the standard library function is like this: void *malloc(size_t size); The free() function takes the pointer returned by malloc() and de-allocates the memory. No indication of success or failure is returned. The function prototype is like this: void free(void *pointer); To illustrate the use of these functions, here is some code to statically defne an array and set the fourth elements value: int my_array[10]; my_array[3] = 99; The following code does the same job using dynamic memory allocation: int *pointer; pointer = malloc(10 * sizeof(int)); *(pointer+3) = 99; The pointer de-referencing syntax is hard to read, so normal array referencing syntax may be used, as [ and ] are just operators: pointer[3] = 99; When the array is no longer needed, the memory may be de-allocated thus: free(pointer); pointer = NULL; Assigning NULL to the pointer is not compulsory, but is good practice, as it will cause an error to be generated if the pointer is erroneous utilized after the memory has been de- allocated. The amount of heap space actually allocated by malloc() is normally one word larger than that requested. The additional word is used to hold the size of the allocation and is for later use by free(). This size word precedes the data area to which malloc() returns a pointer. There are two other variants of the malloc() function: calloc() and realloc(). The calloc() function does basically the same job as malloc(), except that it takes two parameters the number of array elements and the size of each element instead of a single parameter (which is the product of these two values). The allocated memory is also initialized to zeros. Here is the prototype: void *calloc(size_t nelements, size_t elementSize); The realloc() function resizes a memory allocation previously made by malloc(). It takes as parameters a pointer to the memory area and the new size that is required. If the size is reduced, data may be lost. If the size is increased and the function is unable to extend the existing allocation, it will automatically allocate a new memory area and copy data across. In any case, it returns a pointer to the allocated memory. Here is the prototype: void *realloc(void *pointer, size_t size); Dynamic Memory in C++ Management of dynamic memory in C++ is quite similar to C in most respects. Although the library functions are likely to be available, C++ has two additional operators new and delete which enable code to be written more clearly, succinctly and fexibly, with less likelihood of errors. The new operator can be used in three ways: p_var = new typename; p_var = new type(initializer); p_array = new type [size]; In the frst two cases, space for a single object is allocated; the second one includes initialization. The third case is the mechanism for allocating space for an array of objects. The delete operator can be invoked in two ways: delete p_var; delete[] p_array; The frst is for a single object; the second deallocates the space used by an array. It is very important to use the correct de-allocator in each case. There is no operator that provides the functionality of the C realloc() function. Here is the code to dynamically allocate an array and initialize the fourth element: int* pointer; pointer = new int[10]; pointer[3] = 99; Using the array access notation is natural. De-allocation is performed thus: delete[] pointer; pointer = NULL; Again, assigning NULL to the pointer after deallocation is just good programming practice. Another option for managing dynamic memory in C++ is the use the Standard Template Library. This may be inadvisable for real time embedded systems. Issues and Problems As a general rule, dynamic behavior is troublesome in real time embedded systems. The two key areas of concern are determination of the action to be taken on resource exhaustion and nondeterministic execution performance. There are a number of problems with dynamic memory allocation in a real time system. The standard library functions (malloc() and free()) are not normally reentrant, which would be problematic in a multithreaded application. If the source code is available, this should be straightforward to rectify by locking resources using RTOS facilities (like a semaphore). A more intractable problem is associated with the performance of malloc(). Its behavior is unpredictable, as the time it takes to allocate memory is extremely variable. Such nondeterministic behavior is intolerable in real time systems. Without great care, it is easy to introduce memory leaks into application code implemented using malloc() and free(). This is caused by memory being allocated and never being deallocated. Such errors tend to cause a gradual performance degradation and eventual failure. This type of bug can be very hard to locate. Memory allocation failure is a concern. Unlike a desktop application, most embedded systems do not have the opportunity to pop up a dialog and discuss options with the user. Often, resetting is the only option, which is unattractive. If allocation failures are encountered during testing, care must be taken with diagnosing their cause. It may be that there is simply insufcient memory available this suggests various courses of action. However, it may be that there is sufcient memory, but not available in one contiguous chunk that can satisfy the allocation request. This situation is called memory fragmentation. Memory Fragmentation The best way to understand memory fragmentation is to look at an example. For this example, it is assumed hat there is a 10K heap. First, an area of 3K is requested, thus: #defne K (1024) char *p1; p1 = malloc(3*K); Then, a further 4K is requested: p2 = malloc(4*K); 3K of memory is now free. Some time later, the frst memory allocation, pointed to by p1, is de-allocated: free(p1); This leaves 6K of memory free in two 3K chunks. A further request for a 4K allocation is issued: p1 = malloc(4*K); This results in a failure NULL is returned into p1 because, even though 6K of memory is available, there is not a 4K contiguous block available. This is memory fragmentation. It would seem that an obvious solution would be to de-fragment the memory, merging the two 3K blocks to make a single one of 6K. However, this is not possible because it would entail moving the 4K block to which p2 points. Moving it would change its address, so any code that has taken a copy of the pointer would then be broken. In other languages (such as Visual Basic, Java and C#), there are defragmentation (or garbage collection) facilities. This is only possible because these languages do not support direct pointers, so moving the data has no adverse efect upon application code. This defragmentation may occur when a memory allocation fails or there may be a periodic garbage collection process that is run. In either case, this would severely compromise real time performance and determinism. Memory with an RTOS A real time operating system may provide a service which is efectively a reentrant form of malloc(). However, it is unlikely that this facility would be deterministic. Memory management facilities that are compatible with real time requirements i.e. they are deterministic are usually provided. This is most commonly a scheme which allocates blocks or partitions of memory under the control of the OS. Block/partition Memory Allocation Typically, block memory allocation is performed using a partition pool, which is defned statically or dynamically and confgured to contain a specifed number of blocks of a specifed fxed size. For Nucleus OS, the API call to defne a partition pool has the following prototype: STATUS NU_Create_Partition_Pool (NU_PAR TITION_POOL *pool, CHAR *name, VOID *start_address, UNSIGNED pool_size, UNSIGNED partition_size, OPTION suspend_type); This is most clearly understood by means of an example: status = NU_Create_Partition_Pool(&MyPoo l, "any name", (VOID *) 0xB000, 2000, 40, NU_FIFO); This creates a partition pool with the descriptor MyPool, containing 2000 bytes of memory, flled with partitions of size 40 bytes (i.e. there are 50 partitions). The pool is located at address 0xB000. The pool is confgured such that, if a task attempts to allocate a block, when there are none available, and it requests to be suspended on the allocation API call, suspended tasks will be woken up in a frst-in, frst-out order. The other option would have been task priority order. Another API call is available to request allocation of a partition. Here is an example using Nucleus OS: status = NU_Allocate_Partition(&MyPool, &ptr, NU_SUSPEND); This requests the allocation of a partition from MyPool. When successful, a pointer to the allocated block is returned in ptr. If no memory is available, the task is suspended, because NU_SUSPEND was specifed; other options, which may have been selected, would have been to suspend with a timeout or to simply return with an error. When the partition is no longer required, it may be de-allocated thus: status = NU_Deallocate_Partition(ptr); If a task of higher priority was suspended pending availability of a partition, it would now be run. There is no possibility for fragmentation, as only fxed size blocks are available. The only failure mode is true resource exhaustion, which may be controlled and contained using task suspend, as shown. Additional API calls are available which can provide the application code with information about the status of the partition pool for example, how many free partitions are currently available. Care is required in allocating and de-allocating partitions, as the possibility for the introduction of memory leaks remains. Memory Leak Detection The potential for programmer error resulting in a memory leak when using partition pools is recognized by vendors of real time operating systems. Typically, a profler tool is available which assists with the location and rectifcation of such bugs. Real Time Memory Solutions Having identifed a number of problems with dynamic memory behavior in real time systems, some possible solutions and better approaches can be proposed. Dynamic Memory It is possible to use partition memory allocation to implement malloc() in a robust and deterministic fashion. The idea is to defne a series of partition pools with block sizes in a geometric progression; e.g. 32, 64, 128, 256 bytes. A malloc() function may be written to deterministically select the correct pool to provide enough space for a given allocation request. This approach takes advantage of the deterministic behavior of the partition allocation API call, the robust error handling (e.g. task suspend) and the immunity from fragmentation ofered by block memory. Conclusions C and C++ use memory in various ways, both static and dynamic. Dynamic memory includes stack and heap. Dynamic behavior in embedded real time systems is generally a source of concern, as it tends to be non-deterministic and failure is hard to contain. Using the facilities provided by most real time operating systems, a dynamic memory facility may be implemented which is deterministic, immune from fragmentation and with good error handling. Dynamically Allocating Memory Usually when declaring variables using C++, the compiler "tells" the executable file that it creates how much memory is going to be needed to run the program !his memory can then be allocated before the program is run to provide the appropriate space !his is great for the most part as it usually means that most of your data is stored in a similar location, and more importantly it means that you have a big chun" of memory which the compiler manages for you #e haven$t had to really worry about variables we$ve created % we don$t have to worry about deleting memory which we$ve allocated or anything li"e that, the compiler does it all for us &ou can, however, allocate memory dynamically in C++ Dynamic memory allocation is the allocation and ownership of memory for a certain data structure at runtime 'while your program is running, hence dynamically(, and this ma"es flexible data si)es and dynamic ob*ect creation possible 'and easy+( #hen you dynamically allocate memory you$re essentially saying "Hey! Give me my own chunk of memory over here!" %% you can really do whatever you want with this memory however you$re responsible for clearing up 'and freeing( the memory when you$ve finished using it !he topic of dynamic memory allocation behind the scenes is somewhat complex, but in this tutorial we$re *ust going to go over the basics of allocating and using memory "on the fly" using C++, and more specifically, using the new "eyword ,erhaps the simplest example of dynamic memory allocation would be to declare and initiali)e an integer variable dynamically Dynamic memory isn$t accessed li"e "normal variables", but is instead utili)ed through pointers % this is one of the reasons why pointers are such an extremely powerful force in C++ !he basic idea is that we create a new pointer, and then point this to a "new" bloc" of memory which we$ve allocated, and then we can simply use the pointer to write to that memory !his is all done, of course, around the data%type of whatever we want to store -o to follow our concept with an example, a new integer pointer would be declared, and then we would ma"e this point to a new section of memory of si)e sizeof(int) 'which, on most machines, is four bytes( !his new section of memory which we "eep tal"ing about, can be allocated by using the new "eyword, followed by the data%type 'whether a core data%type li"e an integer, or something more abstract li"e a custom struct or class( !he general process of declaring a new pointer, setting it to a newly 'and dynamically( allocated section of memory, and then setting the value at that location, is shown in the diagram below. Dynamically creating an integer variable via a pointer As alluded to in the diagram above, once the pointer is set to a new section of memory we can simply treat it li"e a normal integer pointer % using the dereference operator '*( to set the value at the memory location !his means we can essentially treat it *ust li"e an integer variable if we *ust use the dereference operator As / mentioned earlier, however, we do need to clean up after ourselves /f we$ve allocated four bytes 'or whatever sizeof(int) is( of memory, we need to "delete" this or free it up when we$re finished with it or it will *ust hang around in memory, creating what is "nown as a memory leak !his is done, in this case, by writing the delete "eyword followed by the pointer name % so delete ptr;. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include iostream! using namespace std; int main() " int* ptr # new int; $$Declare and initialize a pointer to some dynamically allocated memory *ptr # %; $$&et our integer '(aria)le' to % ((ia t*e pointer) *ptr +# +%; $$Add +% cout *ptr $$,utput t*e (alue at location -ptr- delete ptr; $$Free up t*e memory we used . we-re done wit* it return /; 0 !he only problem with our examples so far is that, to be fran", they aren$t all that useful 0ur programs are performing tas"s which they could *ust as easily perform using "normal" variables, and the only real differences are behind the scenes 0ne of the "ey use%cases for dynamically allocating memory is when you don$t "now how much memory you$ll need to allocate -ay, for example, we had a program which too" in the number of students in a class and then created an array with this si)e #e cannot, using "normal" techni1ues, create an array with a flexible si)e li"e this, but using dynamically allocated memory, such a tas" could be done with relative ease #e can simply ta"e in the si)e 'straight into an integer variable using cin if we don$t want to worry about string%to%int conversion(, and then create a new integer pointer which points to the start of a new integer array -ince arrays are essentially *ust pointers to the first element of the array, we can simply do this with int* array # new int1size2; 2rom here, we can simply treat the integer pointer li"e an array % we don$t even need to use the dereference operator because the s1uare brac"ets handle all of that for us, arrays *ust being pointers and all #e can then proceed to do whatever the hell we want with the array, and then free up the memory by using the delete "eyword, followed by some empty s1uare brac"ets to indicate that we$re dealing with an array, followed by the pointer 'or "array" if you li"e( name !his is shown in the code snippet as follows. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main() " int size; cout 'Class size3 '; cin !! size; int* array # new int1size2; $$Dynamically allocated 'array' of size -size- for(int i # /; i size; i++) " array1i2 # i++; cout array1i2 ' '; 0 15 16 17 18 19 delete 12 array; return /; 0 #e could, of course, ma"e use of manual pointer arithmetic here if we wanted to use this instead of using the s1uare brac"et notation % but s1uare brac"ets seem to ma"e more sense in this instance /t$s worth noting that if we wanted to, we could actually set the $array$ pointer to another new section of memory to create a different array after we deleted the old one % this techni1ue is utili)ed in the creation of many applications Another common use for dynamic memory allocation is creating ob*ects and other data structures only where it is necessary 3et$s say, for example, we had a really big custom struct that re1uired a bunch of data % perhaps some "ind of video data or something /f we *ust declared the ob*ect normally with something li"e 4eally5ig&truct o)6ect; in our code, the space for this ob*ect would be allocated in preparation for our program to run /f we allocate the memory dynamically however, the memory will only be allocated once we use the new "eyword !a"e for example the following example in which we might not want to create the ob*ect "regularly" as we are. 1 2 3 4 5 6 7 8 9 struct 4eally5ig&truct " long *eig*t7 widt*7 dept*7 metadata; $* 888 etc 888 *$ 0; int main() 10 11 12 13 14 15 16 17 18 19 20 21 22 23 " )ool option # false; if(option) " 4eally5ig&truct o)6ect; 0 else " $$Do ot*er t*ings w*ic* don-t in(ol(e -o)6ect- 0 return /; 0 !he above would be extremely memory inefficient in cases in which $option$ was false, and as such, it would probably be a good idea to dynamically allocate the memory for the 4eally5ig-truct ob*ect !his can be done by simply using the new "eyword *ust li"e we did with the ints at the start of this tutorial % we can simply create a pointer to whatever data%type we want to allocate6store dynamically, and then use the new "eyword. 1 2 3 4 5 6 7 8 9 10 struct 4eally5ig&truct " long *eig*t7 widt*7 dept*7 metadata; $* 888 etc 888 *$ 0; int main() " 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 )ool option # false; if(option) " 4eally5ig&truct* o)6ect # new 4eally5ig&truct; $$Do t*ings w*ic* in(ol(e -o)6ect- delete o)6ect; 0 else " $$Do t*ings w*ic* don-t in(ol(e -o)6ect- 0 return /; 0 !he above is much more memory efficient 7ote that instead of using new and delete, we could ma"e use of the malloc and free C functions which are being used behind the scenes /t can be dangerous to use these, however, as they don$t always behave as expected, and as such /$d recommend staying with new and delete wherever possible 'also, new and delete are really the C++ way of doing things( #e should also 1uic"ly tal" about dynamic memory allocation in classes !here are a number of different approaches to designing classes which ma"e use of dynamic memory allocation % the traditional approach is where the programmer must clean up after the ob*ect, calling 8close() methods and the li"e to clear any memory which class ob*ects have allocated, however this can lead to a number of problems, namely that the programmer can easily ma"e a mista"e and that if something goes horrible wrong before these vital memory clearing function calls, a memory lea" will occur As such, the RAII programming idiom is becoming increasingly popular for C++ class design 4A// stands for Resource Ac1uisition Is Initiali)ation, and the basic idea is that a class$s constructor should deal with dynamic allocation of memory, and that a class$s destructor should deal with the de%allocation of allocated memory !his way, the memory is always de%allocated when the ob*ect is destroyed, and thus memory lea"s are prevented even in exceptional circumstances !his can cause a number of complexities to class design depending on the complexity of the class, but if done right can lead to very good practices for allocating and de%allocating memory in which the programmer doesn$t have to worry 1uite as much about memory lea"s from dynamic memory allocation from class ob*ects, as ob*ects really manage their own memory