Структура переменных раздута, дополнительные дополнения добавляются в конце структуры

#c #templates #tuples #variadic-templates #sizeof

#c #шаблоны #кортежи #вариативные шаблоны #sizeof

Вопрос:

Я следовал этому руководству по созданию вариативной структуры, которое почти идентично другому руководству по созданию элементарного кортежа с нуля. К сожалению, когда я анализирую вариационную структуру, она кажется очень неэффективной. Размер структуры кажется раздутым, так как размер структуры, похоже, не соответствует ее расположению переменных. Не похоже, что выравнивание байтов является проблемой, поскольку фактические кортежи, похоже, не страдают от этого эффекта, поэтому мне было интересно, как они это обходят или что я делаю неправильно в своей структуре.

Ниже приведен код, который я использовал для тестирования структуры переменных:

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

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

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

    DataStructure() {}

    T first;
};

template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
    DataStructure(const Tamp; first, const Restamp; ... 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)amp;std::get<0>(tp) - (long)amp;tp << "(int32) | " << (long)amp;std::get<1>(tp) - (long)amp;tp << "(float) | " << (long)amp;std::get<2>(tp) - (long)amp;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)amp;std::get<0>(tp2) - (long)amp;tp2 << "(double) | " << (long)amp;std::get<1>(tp2) - (long)amp;tp2 << "(float) | " << (long)amp;std::get<2>(tp2) - (long)amp;tp2 << "(int32)" << std::endl;
    std::cout << std::endl;

    std::array<defDouble, 2> arr;
    std::cout << "Array DataStructure<double, float, int32_t> Offsets: [0] - " << (long)amp;arr[0] - (long)amp;arr << ", [1] - " << (long)amp;arr[1] - (long)amp;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
 

Я изменил порядок аргументов шаблона между структурой данных с double и кортежем, потому что кортеж, который я использовал, внутренне меняет порядок элементов (это неуказанная деталь реализации tuple и то, как gcc ее реализовывал); таким образом, оба макета элементов находятся в том же порядке, что и doubleначиная со смещения 0, за которым следует их число с плавающей запятой и, наконец, их int32. Этот макет можно увидеть визуально в структуре test2. Мы видим, что соответствующий размер структуры, как показано с помощью «test2», равен 16, а смещения членов равны 0 для double, 8 для float и 12 для int32. Кортеж показывает то же самое, хотя и с обратным порядком элементов, имеющим double в 0, float в 8, int32 в 12 и общий размер 16 байт. Эти же смещения находятся в структуре данных с double; double имеет смещение 0, значение float равно 8, а int32 равно 12, но на этот раз общий размер составляет 24 байта, и я не могу понять, что дополняется в конце структуры или почему. Я знаю, что удвоения должны быть выровнены по 8 байтам, но здесь это не должно быть проблемой, и явно не так, как показано в случае кортежа и test2 .

Комментарии:

1. @aschepler результаты std::tuple являются последними напечатанными результатами, и для их получения я использовал (long)amp;std::get<0>(tp) - (long)amp;tp , который, казалось, работал, и дал макет (0-7) с двойным (8-11) плавающим числом и (12-15) int32 с общим размером 16 бит.

2. Извините, да, я пропустил, что там было.

3. @AlanBirtles расположение в памяти остается тем же (я изменил его, чтобы сохранить их расположение в памяти), и даже при текущем расположении размер структуры должен составлять 16 байт, а удвоения выровнены по 8 байтов, поэтому не должно быть необходимости в заполнении. Также используется DataStructure<int32_t, float, double> размер структуры данных 32 байта

4. Кажется, я это вижу. Что такое sizeof(DataStructure<>) ?

5. @aschepler sizeof(DataStructure<>) = 1 , что нормально, насколько я знаю

Ответ №1:

Поэтому даже пустому классу требуется место для хранения самого себя, поэтому минимальный размер класса 1 . Поскольку ваш DataStructure класс без аргументов пуст и является членом, он занимает место и заставляет остальные члены занимать больше места для выравнивания. Создание непустой базы устраняет проблему:

 template<typename ... T>
struct DataStructure;

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

    T first;
};

template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
    DataStructure(const Tamp; first, const Restamp; ... 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 байт, чтобы компилятор увеличил это значение до 16 байт по сравнению с тем, что в массиве B , a всегда выровненном по 8 байтам. sizeof(B) следовательно, будет 16, что приведет к sizeof(C) 24.

Я думаю std::tuple , что обычно реализуется с использованием базовых классов, а не членов. Изменение вашей реализации для этого также устраняет эту проблему с заполнением:

 template<typename ... T>
struct DataStructure;

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

    T first;
};

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

Комментарии:

1. Или (если вы используете достаточно новый компилятор в режиме C 20), просто включите [[no_unique_address]] rest .

2. Итак, оба, похоже, отлично работают для вышеуказанной ситуации. Это для DataStructure<double, float, int32> , но затем, когда я меняю double местами и int32 , он возвращается к размеру 24 байта, и новый макет выглядит так: 0(int32), 8(float,) 16(double) . Теперь, почему значение с плавающей точкой должно находиться в позиции 8, а не в позиции 4, почему оно вводит заполнение между int32 и float ? Кажется, что все члены вынуждены соответствовать выравниванию байтов double . Я отредактировал вопрос, включив предложения и распечатку, чтобы продемонстрировать новую проблему внизу.