8 min. read

C++ Objects in memory

Objects in C++ can be mapped into memory and vice versa,
This means memory can be cast into objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A
{
int Value;
int Value2;
};

int main()
{
int memory[2]{ 1,2 };

A representation = *((A*)&memory);

std::cout << representation.Value << "\n";
std::cout << representation.Value2;

}

Basic types size

It’s pretty common to assume int is 32 bit and long is 64 bit.
Most of us are developing on 64 bit machines so we assume:

  • Short is 16 bits.
  • Int is 32 bits.
  • Long and Ptr are 64 bits.

This behavior is controlled by a standard called “Data Model” which defines what is the size of short, int, long and pointer.
We’ll assume here it’s LP64 which as stated int is 32 bits or 4 bytes.

By simply running sizeof(type) we can check what is the data model.
On my machine:

1
2
3
4
5
Short 2
Int 4
Long 4
Long long 8
Ptr 8

Alignment

Alignment of objects is an integer (size_t) that represents what addresses successive object can be mapped to.

The first struct:

1
2
3
4
5
struct A
{
int Value;
int Value2;
};

A has 2 integers that are 4 bytes, which means the alignment of A is 4!
How can we confirm it? Use the alignof operator:

1
2
3
4
5
int main()
{
std::cout << "Alignment of A is " << alignof(A) << "\n";
// Outputs 4.
}

Because A is aligned by 4 it means we need to place the object after 4 bytes.
Because 2 ints consists of 8 bytes the alingment is perfect!

What would happen if we add a char?

1
2
3
4
5
6
struct A
{
int Value;
int Value2;
char Value3;
};

Let’s compare both alignof and sizeof before the change and after:

Before:

1
2
Alingment of Previous A is 4
Size of Previous A is 8

After:

1
2
Alingment of A is 4
Size of A is 12

char is 1 byte size!
So how come the size is increased to 12 and not 9?
This is all because alignment!

The default minimum alignment of this type is 4 therefore we need to accomodate the size.

As alignment defined - This is done to ensure successive objects can be aligned properly in memory:


What would happen if we decid to ignore this rule?

The first line is no longer aligned!

Why do we align to power of 2?

  • This is how it’s built.
    A bit is a binary value - 0 or 1 and types are also the power of 2 -
    Byte - 8 bits,
    Short - 16 bits,
    Int - 32 bits,
    Long long - 64 bits.

To keep it understandable and not counting offsets to uneven objects we need to write even objects.
In 3D graphics we also need power of 2 images to have them align better in memory.

  • Cache lines and Pages.
    A lot of performance optimization is built on the assumption that we cache Power of 2 size bytes.
    Usually a cache line is 64 bytes so whenever the CPU is loading memory into its cache it will load an enitre line.

If we take the last example of structures that are not aligned in memory, imagine loading a cache line with only half the object, how would caching work when we need to loop through objects?

1
2
3
4
5
6
for(auto& AObj : listOfAs)
{
std::cout << AObj.Value << "\n"; // Value is in cache line.
std::cout << AObj.Value2 << "\n"; // Value2 is not fully cache lines.
std::cout << AObj.Value3 << "\n"; // Value3 is not cached.
}

Padding

Padding exists to complete the alignment of classes.

Example 1

1
2
3
4
5
6
7
struct A
{
int Value; // 4 bytes
int Value2; // 4 bytes
char Value3; // 1 bytes
/* Padding of 3*/ // 3 bytes
};

Struct A is 12 bytes and aligned by 4.

Example 2

1
2
3
4
5
6
7
struct A
{
int Value; // 4 bytes
char Value3; // 1 bytes
/* Padding of 3*/ // 3 bytes
int Value2; // 4 bytes
};

We switched the order of char and int does it affect the struct?
Yes and No.

We haven’t changed drastically the alignment but we did change the order, the padding is still 4.

Example 3

1
2
3
4
5
6
7
struct A
{
int Value;
char Value2;
long long Value3;
int Value4;
};

The size is now 24!
Let’s analyze this class:

Value is 4 bytes long.
Value2 is 1 bytes + 3 padding to align to the previous int.
Value3 is 8 bytes long, Value and Value2 both align to 8.
Value4 is 4 bytes - we need to align it to 8 therefore we get another 4 bytes padding.

1
2
3
4
5
6
int 4 bytes
char 1 byte
3 bytes padding
long long 8 bytes
int 4 bytes
4 bytes padding

4 + 1 + 3 + 8 + 4 + 4 = 24

What’s the minimum alignment now?

alingas

To request to align the struct in a different manner we can use the alignas operator:

1
2
3
4
5
6
7
struct alignas(16) A
{
int Value;
char Value2;
long long Value3;
int Value4;
};

Now struct A is aligned to 16, total size is - 32 bytes.

Why do we need different alignment?
Basically it all comes down to - Performance.

When the CPU loads cache lines or the OS load pages it want to load as much memory as it can in little effort - so when we jump through memory too much we create many misses which affect performance.

This is observed in the previous example:

Imagine our cache lines are 24 bytes - To access the 3rd object we’ll need to load 2 cache lines.
Because each line is 24 bytes and the object is aligned to 9 bytes.

To align it to 24 bytes we’ll need to hold 2 objects - 18 bytes and keep the rest of the bytes empty - 6 bytes.

What issue empty memory causes?
Fragmentation issues!
For each 2 objects we lose 6 bytes.
For 100 objects we lose 600 bytes!


That’s why classes are aligned by default.
Just make sure your classes are ordered correctly to avoid unnecessary padding!

Unordered:

1
2
3
4
5
6
7
8
struct A
{
int Value;
bool Value2;
int Value3;
char Value4;
short Value5;
};
1
2
Alingment of A is 4
Size of A is 16

Ordered:

1
2
3
4
5
6
7
8
struct A
{
int Value;
bool Value2;
int Value3;
char Value4;
short Value5;
};
1
2
Alingment of A is 4
Size of A is 12

When creating types be attentive to paddings,
A simple rule to go by is to order them from biggest to smallest to avoid paddings.

Thanks for reading!


Is this helpful? You can show your appreciation here: Buy me a coffee