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;
}
If we replace
double *arr = (double*) malloc(n * sizeof(double));
with
# DEFINE DBL_SIZE 8
// previous code
double *arr = (double*) malloc(n * DBL_SIZE);
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));
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);
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;
}
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
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));
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));
This will output
Size of person: 52 bytes
Size of name: 50 bytes
Size of age: 4 bytes
Size of height: 4 bytes
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)
The format specifier for
sizeof()
that returnssize_t
is%zu
, not%d
as you used in your example. Similarly,num_elements
should also besize_t
, notint
.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.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
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.
There are no more steps to get the data.
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).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.
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.
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?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 havestruct
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.