DEV Community

PGzlan
PGzlan

Posted on

Under the hood: The Importance of sizeof in C and C++

In typed programming languages, data types play a very important role because they allow programmers to write efficient and reliable code without worrying about the underlying details of how that data type is mapped into a memory. The compiler will allocate the proper amount of memory for that variable.

Different data types can take up varying amounts of memory. Take an integer as an example, this data type usually takes up 4 bytes of memory (DWORD in x86 architecture), while a floating-point data type can take up 8 bytes (or more, depending on what compiler you are using, on that offers a lot of flexibility is GMP)

Having said that, it makes much sense to use the operator (or function, depending on how you like to view it) sizeof. Imagine you want to write a code that allocates an array of doubles using malloc. The code would look something like this:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv) {
    int n = 5;
    double *arr = (double*) malloc(n * sizeof(double));

    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // initialize and print array
    for (int i = 0; i < n; i++) {
        arr[i] = i * 1.0f;
        printf("%lf ", arr[i]);
    }
    printf("\n");

    // free memory to avoid memory leaks
    free(arr);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

If we replace

    double *arr = (double*) malloc(n * sizeof(double));
Enter fullscreen mode Exit fullscreen mode

with

    # DEFINE DBL_SIZE 8
    // previous code
    double *arr = (double*) malloc(n * DBL_SIZE);
Enter fullscreen mode Exit fullscreen mode

This could cause an issue if this code is compiled by a different compiler or a different architecture. While this may not be apparent in this simple example, the next example(s) will help demonstrate the benefits of using sizeof

Memory Management

One of the primary uses of sizeof is in memory management. Whenever we declare a variable in our program, the compiler allocates a certain amount of memory for it based on its data type. The sizeof operator allows us to determine the size of this memory allocation.

For example, let's say we declare an integer variable called number in our program. We can use the sizeof operator to determine the size of this variable in bytes:

int number;
printf("Size of number: %d bytes\n", sizeof(number));
Enter fullscreen mode Exit fullscreen mode

This will print "Size of number: 4 bytes" on most modern systems, as an integer is typically 4 bytes in size. We can use this information to ensure that we allocate the appropriate amount of memory for our variables and to optimize our program's memory usage.

Data Manipulation

One other important usage of sizeof is in data manipulation. Using sizeof operator can help us determine the size of arrays, structures, and other complex data types. This information can be used to manipulate these data types in various ways.

Let's break down each type with an example

Arrays

For example, let's say we have an array of integers called numbers. We can use the sizeof operator to determine the number of elements in this array:

int numbers[10];
int num_elements = sizeof(numbers) / sizeof(numbers[0]);
printf("Number of elements in array: %d\n", num_elements);
Enter fullscreen mode Exit fullscreen mode

This will produce the following output "Number of elements in array: 10", as there are 10 elements in the array. This information can be very useful for any operations that we want to do on any of the array elements (traversal, sorting, etc).

Thing might appear trivial in case of arrays, nevertheless, let's see the following example as it shows an interesting behavior

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n = 5;
    double *arr = (double*) malloc(n * sizeof(double));

    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // initialize array and print original array
    printf("Original array: ");
    for (int i = 0; i < n; i++) {
        arr[i] = (i + 1) * 1.0f;
        printf("%lf ", arr[i]);
    }
    printf("\n");

    // reallocate array using realloc()
    size_t new_n = 2 * (sizeof(arr) / sizeof(arr[0]));
    arr = (double*) realloc(arr, new_n * sizeof(double));

    if (arr == NULL) {
        printf("Memory reallocation failed!\n");
        return 1;
    }

    // print new array
    printf("New array: ");
    for (int i = 0; i < new_n; i++) {
        printf("%lf ", arr[i]);
    }
    printf("\n");

    // free memory
    free(arr);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The output of this code in the current form will be

Original array: 1.000000 2.000000 3.000000 4.000000 5.000000 
New array: 1.000000 2.000000 
Enter fullscreen mode Exit fullscreen mode

This may seem strange initially but it's actually very straight forward. sizeof works if the size is known at compile time and to create dynamic arrays, you usually use a pointer (which will have a different size depending on the architecture of the CPU used), the size of a pointer is fixed, regardless of the data type. So when we create call sizeof on arr, it returns 8 (the size of the pointer) and the size of arr[0] is 8 (double), this yields (2 * (8/8)) which gives 2, that's why the new array only has two elements.

The main point here is that for dynamic arrays, you should store the size of the array somewhere as using sizeof will yield incorrect results

Structures

Similarly, we can use the sizeof operator to determine the size of a structure in bytes:

struct person {
  char name[50];
  int age;
  float height;
};
struct person john;
printf("Size of person struct: %d bytes\n", sizeof(john));
Enter fullscreen mode Exit fullscreen mode

This will output "Size of person struct: 60 bytes". This looks odd, doesn't it? We know that the structure contains a char array of 50 bytes, an integer of 4 bytes, and a float of 4 bytes, this should add up to 58 (50 + 4 + 4), yet it shows 60 bytes so why is that? Let's see if unions exhibit a similar behavior

Unions

Unions are another complex data type that can benefit from the sizeof operator. Unions allow us to store different data types in the same memory location. We can use the sizeof operator to determine the size of a union and its members.

For example, let's say we have a union called my_union that can hold either an integer or a float. We can use the sizeof operator to determine the size of the union and its members:

union person {
    char name[50];
    int age;
    float height;
};
union person p;
printf("Size of person: %d bytes\n", sizeof(p));
printf("Size of name: %d bytes\n", sizeof(p.name));
printf("Size of age: %d bytes\n", sizeof(p.age));
printf("Size of height: %d bytes\n", sizeof(p.height)); 
Enter fullscreen mode Exit fullscreen mode

This will output

Size of person: 52 bytes
Size of name: 50 bytes
Size of age: 4 bytes
Size of height: 4 bytes
Enter fullscreen mode Exit fullscreen mode

52? That looks stranger than the case with the struct.

What is going on?

In C/C++, structs and unions are used to group related data together which is why how the data is aligned in memory can have a significant impact on program performance.

Memory alignment refers to the process of aligning data in memory so that it can be accessed more efficiently by the CPU. This is achieved by padding the memory with extra bytes so that each data element is aligned to a memory address that is a multiple of its size.

The padding ensures that the data elements are accessed efficiently by the CPU, which can result in faster program execution. The downside of padding is the wasted memory space, which some call memory overhead.

To reduce memory overhead, the concept of packing is used, which involves reducing the amount of padding by rearranging the order of data elements.

When padding data elements, the C standard also specifies alignment requirements for the beginning address of a struct or union. The standard mandates that the starting address of a struct or union must be aligned to the boundary of the largest member in the struct or union. This ensures that the entire struct or union is properly aligned in memory.

In the case of the person union, the largest data type has a size of 4 bytes. If we divide 50 by 4, it'll give us 12, which is 48 bytes. We have two bytes remaining in case we use the name member of the union, and since the largest data type is 4, then 48 + 4 = 52

The case of the struct is very similar to case of the union and since the struct encompasses all elements, the result will be (48 + 2 + 2 (alignment) + 4 + 4) which is equal to 60.

Consider a simpler struct that contains a double (which typically requires 8 bytes of memory) and an int (which typically requires 4 bytes of memory), then the struct itself must be aligned on an 8-byte boundary. This means that the starting address of the struct must be a multiple of 8. If the starting address of the struct is not aligned to 8 bytes, then the CPU may need to perform additional operations to extract the data, which can lead to slower program execution.

In addition to the C standard, some processors may have their own specific alignment requirements for optimal performance. It is important for C programmers to be aware of any processor-specific alignment requirements and to take them into consideration when designing and implementing their programs.

Conclusion

In conclusion, the sizeof operator is an essential tool for C and C++ programming. It allows us to determine the size of variables and data types, manipulate data, and optimize our programs. By using the sizeof operator effectively, we can ensure that our programs are using memory efficiently and performing at their best.

Some tips to use sizeof effectively in your code:

  • Always use sizeof when allocating memory for a data type.
  • Use sizeof to determine the size of a structure or union.
  • Avoid hard-coding values in your code by using sizeof instead.

By following these tips, you can write more efficient and maintainable code in C and C++.

References

https://stackoverflow.com/questions/2117486/c-pointer-arithmetic
https://stackoverflow.com/questions/14171117/implementation-of-sizeof-operator
https://www.c-faq.com/struct/align.html
https://en.wikipedia.org/wiki/Sizeof
https://cplusplus.com/forum/beginner/279863/
https://stackoverflow.com/questions/14004704/find-malloc-array-length-in-c
https://stackoverflow.com/questions/891471/union-element-alignment
https://stackoverflow.com/questions/16703211/why-128bit-variables-should-be-aligned-to-16byte-boundary

Top comments (5)

Collapse
 
pauljlucas profile image
Paul J. Lucas

The format specifier for sizeof() that returns size_t is %zu, not %d as you used in your example. Similarly, num_elements should also be size_t, not int.

Padding generally has nothing to do with efficiency. It has to do with the CPU being able to access data at all since it generally must be aligned properly. So the choice is not efficient vs inefficient; it's work vs. bus error.

sizeof is sometimes a run-time operator, specifically when the argument is a variable length array (in C99 and later). See here for details.

Collapse
 
0xog_pg profile image
PGzlan

How is it not inefficient when I am increasing the size of structure and still requiring possibly more steps to get the desired data? Whether it's caching or larger memory that could be wasteful when communicating over a network or serializing data? If it did not make some form in "inefficiency", why would some compilers bother to reorder them in order them instead of just directly pad them. Surely one of them is more sophisticated than the other

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

How is it not inefficient when I am increasing the size of structure ...

I'm saying efficient is not relevant if the alternative is bus error. For an analogy, it would be like you driving and got a flat tire, get out of your car, and it catches on fire. Yes, your tire is still flat, but the fact that your car is on fire is your bigger problem. Some CPUs simply can not access unaligned data.

... requiring possibly more steps to get the desired data?

There are no more steps to get the data.

Whether it's caching or larger memory that could be wasteful ...

Again, you have no choice. If you want to save memory, lay out your structure members sorted by sizeof(T), descending. In some cases, you might not have a choice: you might need to conform to some specific data layout (padding included).

... when communicating over a network or serializing data?

Serialization is a completely separate thing. When you serialize, you generally convert the in-memory representation into an over-the-wire representation (without the padding). But there could be exceptions if you want better performance among homogenous computers on an intranet where you might transmit the padding too if it means that no serialization or deserialization steps are needed. There are always trade-offs.

If it did not make some form in "inefficiency", why would some compilers bother to reorder them in order them instead of just directly pad them.

I don't know what that means. No C compiler will reorder your structure members for you. The order you put them in is always the order in which they are laid out in memory.

Thread Thread
 
0xog_pg profile image
PGzlan • Edited

I don't remember where I had read that some compilers do reorder struct elements in the order you described and upon inspections. Isn't that achievable through something like the packed attribute?

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

There might be at least one compiler in existence that has an option to reorder your struct members by size, but then you're no longer programming in standard C. If it matters, you should just always do it yourself.

packed is non-standard and does exactly and only what it says: "pack" != "reorder". If you have struct members A, B, and C (in that order), packed will remove any padding between A and B and between B and C, but they'll remain in the same order. However, the price you pay is that accessing unaligned data (on CPUs that support it) invariably takes more clock cycles and can cross cache lines. Again, there's always a trade-off.

Reordering only, well, reorders and has nothing to do with packing. However, it's often the case that reordering will eliminate (or at least minimize) padding, but it's only because things just so happen to align. Reordering is preferred since you're still programming in standard C.

Unless you have large structures or lots of them to the point where the extra memory use actually matters, it's generally not worth caring about.