简体   繁体   中英

Why does an uint64_t needs more memory than 2 uint32_t's when used in a class? And how to prevent this?

I have made the following code as an example.

#include <iostream>

struct class1
{

    uint8_t     a;
    uint8_t     b;
    uint16_t    c;
    uint32_t    d;
    uint32_t    e;

    uint32_t    f;
    uint32_t    g;
};

struct class2
{

    uint8_t     a;
    uint8_t     b;
    uint16_t    c;
    uint32_t    d;
    uint32_t    e;

    uint64_t    f;
};

int main(){
    std::cout << sizeof(class1) << std::endl;
    std::cout << sizeof(class2) << std::endl;
    std::cout << sizeof(uint64_t) << std::endl;
    std::cout << sizeof(uint32_t) << std::endl;
}

prints

20
24
8
4

So it's fairly simple to see that one uint64_t is as large as two uint32_t's, Why would class 2 have 4 extra bytes, if they are the same except for the substitution of two uint32_t's for an uint64_t.

As it was pointed out, this is due to padding .

To prevent this, you may use

#pragma pack(1)

class ... {

};
#pragma pack(pop)

It tells your compiler to align not to 8 bytes, but to one byte. The pop command switches it off (this is very important, since if you do that in the header and somebody includes your header, very weird errors may occur)

The rule for alignment (on x86 and x86_64) is generally to align a variable on it's size .

In other words, 32-bit variables are aligned on 4 bytes, 64-bit variables on 8 bytes, etc.

The offset of f is 12, so in case of uint32_t f no padding is needed, but when f is an uint64_t , 4 bytes of padding are added to get f to align on 8 bytes.

For this reason it is better to order data members from largest to smallest . Then there wouldn't be any need for padding or packing (except possibly at the end of the structure).

Why does an uint64_t needs more memory than 2 uint32_t's when used in a class?

The reason is padding due to alignment requirements.

On most 64-bit architectures uint8_t has an alignment requirement of 1, uint16_t has an alignment requirement of 2, uint32_t has an alignment requirement of 4 and uint64_t has an alignment requirement of 8. The compiler must ensure that all members in a structure are correctly aligned and that the size of a structure is a multiple of it's overall alignment requirement. Furthermore the compiler is not allowed to re-order members.

So your structs end up laid out as follows

struct class1
{
    
    uint8_t     a; //offset 0
    uint8_t     b; //offset 1
    uint16_t    c; //offset 2
    uint32_t    d; //offset 4
    uint32_t    e; //offset 8
    
    uint32_t    f; //offset 12
    uint32_t    g; //offset 16
}; //overall alignment requirement 4, overall size 20.

struct class2
{
    
    uint8_t     a; //offset 0
    uint8_t     b; //offset 1
    uint16_t    c; //offset 2
    uint32_t    d; //offset 4
    uint32_t    e; //offset 8
    // 4 bytes of padding because f has an alignment requirement of 8
    uint64_t    f; //offset 16
}; //overall alignment requirement 8, overall size 24

And how to prevent this?

Unfortunately there is no good general solution.

Sometimes it is possible to reduce the amount of padding by re-ordering fields, but that doesn't help in your case. It just moves the padding around in the structure. A structure with a field requiring 8 byte alignment will always have a size that is a multiple of 8. Therefore no matter how much you rearrange the fields your structure will always have a size of at least 24.

You can use compiler-specific features such as #pragma pack or __attribute((packed)) to force the compiler to pack the structure more tightly than normal alignment requirements would allow. However, as well as limiting portability, this creates a problem when taking the address of a member or binding a reference to the member. The resulting pointer or reference may not satisfy the alignment requirements and therefore may not be safe to use.

Different compilers vary in how they handle this problem. From some playing around on godbolt.

  • g++ 9 through 11 will refuse to bind a reference to a packed member and give a warning when taking the address.
  • clang 4 through 11 will give a warning when taking the address, but will silently bind a reference and pass that reference across a compilation unit boundary.
  • Clang 3.9 and earlier will take the address and bind a reference silently.
  • g++ 8 and earlier and clang 3.9 and earlier (down to the oldest version on godbolt) will also refuse to bind a reference, but will take the address with no warning.
  • icc will bind a pointer or take the address without producing any warnings in either case (though to be fair intel processors support unaligned access in hardware).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM