Memory Management
An advantage of C programming languages over other languages is that the user may optimize and be creative with memory management, whereas in Python or R, the memory management is done automatically. For high-performance computing users, it allows users to optimize their programs to the extreme and efficiently use their HPC resources. In this section, we will introduce the process of dynamic memory allocation.
In C, arguments to functions are passed by value. When we want to pass a large C structure into a function, that value is copied in the memory for the following function. If many functions take the same data as arguments, the same data will occupy several locations in memory, which is undesirable for performance and memory usage. To avoid this redundancy, we can use dynamic memory allocation. The idea is to allocate a chunk of data in memory of just the right size to store this data. Instead of passing the data to functions, we pass a pointer to the data. This reduces the overhead of copying large amounts of data and ensures only one copy exists in the memory. As a result, the data is now shared between all functions, and all changes are shared. Memory usage is more efficient because only one instance of the data exists. Dynamic memory allocation involves assigning memory as needed during program execution. In contrast, static memory allocation assigns memory at compile time, with the memory lasting until the program or scope ends. With the correct implementation of memory management, the C program may see significant performance improvement.
Here is how to allocate memory and work with pointers:
Suppose we want to allocate enough memory for an integer array of size 10. We use the function malloc
to dynamically allocate memory. Although other memory management functions exist, malloc
is the most common. The argument to malloc
is the size of the memory to be allocated, which can be considered as the number of items times the size of each item. For convenience, the sizeof
function can be used to calculate the size of types or structures. If malloc
is successful, it will return a generic pointer void *
. In this example, we want to cast it to int *
, so we place (int *)
in front to correctly cast the pointer to the right type.
We should always check malloc
is successful, as there are several scenarios where allocation might fail. To confirm if arr
has a pointer after malloc, check if the arr
is NULL
.
We can assign values to arr
just like statically allocated arrays.
When arr
is no longer needed, we should call free
on arr
to free the memory, so other data may be allocated in its place.
Although memory management in C is beneficial, there are some common pitfalls.
-
Using
malloc
when it's not necessary: In the example above, we used a small array as a demonstration, andarr
was not passed to any functions or used in any meaningful way. Memory allocation, in this case, would be impractical, as none of its advantages were utilized. -
We must check if memory allocation is successful: If there is not enough memory or if memory fragmentation (there are no contiguous blocks of memory that can fit your data) occurs, then memory allocation will fail, and the program will crash upon referencing a
NULL
pointer. -
Always call
free
on unused pointers, as too many unfreed memory allocations can cause a memory leak. In addition, referencing the pointer after freeing, freeing a pointer twice, and accessing memory beyond the bounds will result in errors and unexpected behaviors.