Variadic Struct 是臃肿的,在结构的末尾添加了额外的填充

Variadic Struct is Bloated, Extra Padding Being Added At The End of the Struct

我一直在关注 this tutorial for creating a variadic structure, which is nearly identical to another tutorial 从头开始​​创建基本元组。不幸的是,当我分析可变参数结构时,它似乎效率很低。结构的大小似乎臃肿,因为结构的大小似乎与其变量布局不匹配。字节对齐似乎不是问题,因为实际的元组似乎没有受到这种影响,所以我想知道它们是如何绕过它的,或者我在我的结构中做错了什么。

下面是我用来测试可变参数结构的代码:

#include <iostream>
#include <tuple>
#include <array>

template<typename ... T>
struct DataStructure
{
};

template<typename T>
struct DataStructure<T> {
    DataStructure(const T& first) : first(first)
    {}

    DataStructure() {}

    T first;
};

template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
    DataStructure(const T& first, const Rest& ... rest)
        : first(first)
        , rest(rest...)
    {}

    DataStructure() {}
    
    T first;
    [[no_unique_address]] DataStructure<Rest ... > rest;
};

struct test1 {
    int one;
    float two;
};

struct test2 {
    double three;
    float two;
    int one;
};

int main()
{
    std::cout << "Size of test1 with double: " << sizeof(test1) << std::endl;
    std::cout << "Offset of test1 with double: " << offsetof(test1, one) << " | " << offsetof(test1, two) << std::endl;
    std::cout << std::endl;

    typedef DataStructure<int32_t, float> def;
    std::cout << "Size of DataStructure<int32_t, float> w/o Double: " << sizeof(def) << std::endl;
    std::cout << "Offset of DataStructure<int32_t, float> w/o Double: " << offsetof(def, first) << " | " << offsetof(def, rest.first) << std::endl;
    std::cout << std::endl;

    std::cout << "Size of test2 with double: " << sizeof(test2) << std::endl;
    std::cout << "Offset of test2 with double: " << offsetof(test2, one) << "(int32) | " << offsetof(test2, two) << "(float) | " << offsetof(test2, three) << "(double)" << std::endl;
    std::cout << std::endl;

    typedef DataStructure<double, float, int32_t> defDouble;
    std::cout << "Size of DataStructure<double, float, int32_t>: " << sizeof(defDouble) << std::endl;
    std::cout << "Offset of DataStructure<double, float, int32_t>: " << offsetof(defDouble, rest.rest.first) << "(int32) | " << offsetof(defDouble, rest.first) << "(float) | " << offsetof(defDouble, first) << "(double)" << std::endl;
    std::cout << std::endl;

    std::tuple<int32_t, float, double> tp;
    std::cout << "Size of tuple with double (gcc compiled tuple reverses parameter layout in memory): " << sizeof(tp) << std::endl;
    std::cout << "Offset of tuple with double: " << (long)&std::get<0>(tp) - (long)&tp << "(int32) | " << (long)&std::get<1>(tp) - (long)&tp << "(float) | " << (long)&std::get<2>(tp) - (long)&tp << "(double)" << std::endl;
    std::cout << std::endl;

    std::cout << "Size of no parameter DataStructure<>: " << sizeof(DataStructure<>) << std::endl;

    typedef DataStructure<int32_t, float, double> defDoubleNormal;
    std::cout << "Size of DataStructure<int32_t, float, double>: " << sizeof(defDoubleNormal) << std::endl;
    std::cout << "Offset of DataStructure<int32_t, float, double>: " << offsetof(defDoubleNormal, first) << "(int32) | " << offsetof(defDoubleNormal, rest.first) << "(float) | " << offsetof(defDoubleNormal, rest.rest.first) << "(double)" << std::endl;
    std::cout << std::endl;

    std::tuple<double, float, int32_t> tp2;
    std::cout << "Size of tuple with double (gcc compiled tuple reverses parameter layout in memory): " << sizeof(tp) << std::endl;
    std::cout << "Offset of tuple with double: " << (long)&std::get<0>(tp2) - (long)&tp2 << "(double) | " << (long)&std::get<1>(tp2) - (long)&tp2 << "(float) | " << (long)&std::get<2>(tp2) - (long)&tp2 << "(int32)" << std::endl;
    std::cout << std::endl;

    std::array<defDouble, 2> arr;
    std::cout << "Array DataStructure<double, float, int32_t> Offsets: [0] - " << (long)&arr[0] - (long)&arr << ", [1] - " << (long)&arr[1] - (long)&arr;
}

上面的代码打印:

Size of test1 with double: 8
Offset of test1 with double: 0 | 4

Size of DataStructure<int32_t, float> w/o Double: 8
Offset of DataStructure<int32_t, float> w/o Double: 0 | 4

Size of test2 with double: 16
Offset of test2 with double: 12(int32) | 8(float) | 0(double)

Size of DataStructure<double, float, int32_t>: 16
Offset of DataStructure<double, float, int32_t>: 12(int32) | 8(float) | 0(double)

Size of tuple with double (gcc compiled tuple reverses parameter layout in memory): 16
Offset of tuple with double: 12(int32) | 8(float) | 0(double)

Size of no parameter DataStructure<>: 1
Size of DataStructure<int32_t, float, double>: 24
Offset of DataStructure<int32_t, float, double>: 0(int32) | 8(float) | 16(double)

Size of tuple with double (gcc compiled tuple reverses parameter layout in memory): 16
Offset of tuple with double: 8(double) | 4(float) | 0(int32)

Array DataStructure<double, float, int32_t> Offsets: [0] - 0, [1] - 16

我颠倒了 DataStructure with double 和元组之间的模板参数的顺序,因为我在内部使用的元组颠倒了成员的顺序(这是元组的一个未指定的实现细节,也是 gcc 实现它的方式);这样,两个成员布局的顺序相同,double 从偏移量 0 开始,然后是它们的 float,最后是它们的 int32。在 test2 结构中可以直观地看到该布局我们可以看到,“test2”所示结构的适当大小为 16,成员的偏移量对于双精度为 0,对于浮点为 8,对于 12对于 int32。元组显示相同的东西,尽管成员顺序颠倒,double 为 0,float 为 8,int32 为 12,总大小为 16 字节。这些相同的偏移量在 DataStructure 中用 double 找到; double 的偏移量为 0,float 的偏移量为 8,int32 的偏移量为 12,但这次总大小为 24 字节,我无法理解在结构末尾填充的内容或原因。我知道双打需要 8 字节对齐,但这在这里应该不是问题,而且显然不是元组和 test2 的情况。

即使是一个空的class也需要space来存储它自己,因此class的最小大小是1。由于您的无参数 DataStructure class 是空的并且它占用了一个成员 space 并导致其余成员占用更多 space 以允许对齐。使基础非空解决了这个问题:

template<typename ... T>
struct DataStructure;

template<typename T>
struct DataStructure<T>
{
    DataStructure(const T& first)
        : first(first)
    {}

    T first;
};

template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
    DataStructure(const T& first, const Rest& ... rest)
        : first(first)
        , rest(rest...)
    {}
    
    T first;
    DataStructure<Rest ... > rest;
};

这仍然在 DataStructure<int32_t, float, double> 的情况下引入了额外的填充。这是因为您的代码本质上会产生:

struct A
{
    double a;
};

struct B
{
    A a;
    float b;
};

struct C
{
    B b;
    int32_t c;
};

编译器将尝试始终将 double 放在 8 字节的倍数上,因为 B 需要 12 个字节,编译器将其填充到 [=17] 数组中的 16 个字节=], a 始终对齐到 8 个字节。 sizeof(B) 因此将是 16,导致 sizeof(C) 是 24。

我认为 std::tuple 通常是使用基础 classes 而不是成员来实现的。更改您的实现以执行此操作也可以解决此填充问题:

template<typename ... T>
struct DataStructure;

template<typename T>
struct DataStructure<T>
{
    DataStructure(const T& first)
        : first(first)
    {}

    T first;
};

template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>: DataStructure<Rest ... >
{
    DataStructure(const T& first, const Rest& ... rest)
        : first(first), DataStructure<Rest ... >(rest...)
    {}
    
    T first;
};