Advertisement

Placing things into memory

Started by December 04, 2024 06:56 PM
6 comments, last by Calin 1 week, 4 days ago

The school teaching is that an array of something is a continuous strip in memory.

I understand the basic type scenario int Test[2]; ends up in memory as being 4byte4byte (with nothing else in between). But how about the user defined types

 struct AStructure
{
int A;
char B;
char C;
} 

Does AStructure Test[2] make a continuous memory strip like this? 4byte1byte1byte4byte1byte1byte

And what about classes, how do class functions get stored right next to basic types and user defined types.

My project`s facebook page is “DreamLand Page”

Did you say this is a discussion/lesson related with something being taught in your school?

Dev careful. Pixel on board.

Advertisement

Calin said:

Does AStructure Test[2] make a continuous memory strip like this? 4byte1byte1byte4byte1byte1byte

You can use sizeof(), alignof() and offsetof() keywords to figure out yourself.
(It depends on hardware and compiler settings.)

User-defined structures may be padded depending on the compiler settings and platform. In your example, the layout of AStructure would most likely be [A(4), B(1), C(1), padding(2)]. The size of the structure is usually padded so that its members would be correctly aligned if the structure were used in an array. In this case, int would probably have 4-byte alignment, and so there would need to be 2 bytes of padding so that in an array the A member of AStructure begins on a 4-byte boundary.

There are other places than the end where padding can exist. Some examples:

struct BStructure
{
	char A;
	int B;
	char C;
};
struct BStructure // (compiled)
{
	char A;
	char padding[3];
	int B;
	char C;
	char padding[3];
};

struct CStructure
{
	int A;
	long long B; // 8 bytes
};
struct CStructure // (compiled)
{
	int A;
	char padding[4];
	long long B;
};

// Padding can be disabled by telling compiler to pack members to 1-byte alignment. This may have a performance hit, but is useful for consistency accross platforms and DLL boundaries.
#pragma pack(1)
struct BStructure
{
	char A;
	int B;
	char C;
};
struct BStructure // (compiled)
{
	char A;
	int B;
	char C;
};

Generally you want to order the members of your structs/classes so that the larger ones come first. This reduces the amount of padding that the compiler inserts.

Class functions are not stored in the class object. They are stored in a global location in the executable, shared among all class instances. You can interpret normal (non-virtual) member functions as being implemented like this:

struct MyStruct
{
	void doThing( int a, int b );
	int A;
	int B;
};
// Compiler implementation
void MyStruct_doThing( MyStruct* thisPointer, int a, int b );

Virtual functions require the insertion of a vtable address in the class object, so that the function address can be determined for a class object.

struct MyVirtualStruct
{
	virtual ~MyVirtualStruct() {} // Don't forget this
	virtual void doThing( int a, int b );
	
	int A;
	int B;
};

// Compiler implementation
struct MyVirtualStruct
{
	void** vtable; // initialized to address of MyVirtualStruct_vtable
	int A;
	int B;
};
// global vtable for MyVirtualStruct
void* MyVirtualStruct_vtable[2] = 
{
	MyVirtualStruct::~MyVirtualStruct,
	MyVirtualStruct::doThing
};
// Call virtual function "doThing" by getting vtable address, and using the entry index corresponding to "doThing".
MyVirtualStruct instance;
((void(*)(int,int))instance.vtable[1])( &instance, a, b )

//-------------------------------------------

// If we had a child type that overrided "doThing", it gets its own vtable, which has entries for all parent class virtual functions, as well as any child virtual functions (added at the end).
struct MyChildVirtualStruct : public MyVirtualStruct
{
	virtual void doThing( int a, int b ) override;
	virtual void doThing2( int a, int b );
	int C;
	int D;
};
// Compiler Implementation
struct MyChildVirtualStruct
{
	void** vtable; // initialized to address of MyChildVirtualStruct_vtable
	int A;
	int B;
	int C;
	int D;
};
// global vtable for MyVirtualStruct
void* MyChildVirtualStruct_vtable[3] = 
{
	MyVirtualStruct::~MyVirtualStruct, // Use base destructor directly, since we didn't define one
	MyChildVirtualStruct::doThing,
	MyChildVirtualStruct::doThing2
};

NubDevice said:

Did you say this is a discussion/lesson related with something being taught in your school?

What I meant is

They teach you in school that an array of something is a continuous strip in memory

My project`s facebook page is “DreamLand Page”

Some details are specified, some details are implementation defined.

An int is typically 32 bits these days, but can be 16 bit, and can be other sizes. A char is almost always 8 bits, unless you're dealing with unusual hardware or special compiler settings. There are many systems with strict alignment requirements so variables need to be placed in specific addresses based on the variable size (e.g. possibly placed on 2 byte boundaries, 4 byte boundaries, or 16 byte boundaries) or instructions using the address will crash. The x86 family allows for most integer operations to take place at any boundary, although some will have a performance hit if they cross cache memory lines or have other unexpected packing.

Padding in structures is also implementation defined, and there are hardware reasons for different chipsets to do things differently. The x86 family is more generous towards alignment than others through history.

Your structure for the array is probably laid out as you described it:

{
	int A; // 4 bytes
	char B; // 1 byte, no padding ... or maybe 4 bytes total with 3 bytes padding depending on compiler options.
	char C; // 1 byte, no padding ... or maybe 4 bytes total with 3 bytes padding depending on compiler options.
	// If not padded above then probably 2 bytes of padding invisible to you to make the entire structure fit nicely.
} // Could be 6 bytes per entry if the struct is packed tight. Or it could be 8 bytes per entry if B & C were 1 byte each
  // with two bytes padding on the struct, or 12 bytes per entry if B&C were 4 bytes each, or even 16 bytes per entry 
  // if the compiler decides to pad it out so each struct instance is on a 64-bit boundary.

If you arranged it differently you could get a different amount of padding. For example, putting them char, int, char could give this pattern:

{
	char A; // 1 byte
	// 3 bytes invisible padding
	int B; // 4 bytes
	char C; // 1 byte
	// 3 bytes of invisible padding
} // 12 bytes per entry if this is the pattern the compiler uses.

Compilers often allow switches to change structure and array packing options. The two above I just tried in the compiler explorer, to easily verify the layout. See https://godbolt.org/z/vPTqfhvsh If you flip through a few different platforms there are some different settings. You can also change the packing options like /Zp1 /Zp2 /Zp4, /Zp8 and /Zp16 to see how it changes padding in Visual C++. For example Visual C++ /Zp1 (single byte alignment) reduces both structures to 6 bytes with no padding, with no additional padding.

It is typically most efficient to order the elements largest to smallest.

Advertisement

Thank you guys for your useful feedback. I think I've learned a few more things in this thread.

My project`s facebook page is “DreamLand Page”

Advertisement