Подтвердить что ты не робот

Структурирование Переупорядочение по компилятору

Предположим, что у меня есть такая структура:

struct MyStruct
{
  uint8_t var0;
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

Это, возможно, собирается растратить кучу (ну не тонну) пространства. Это связано с необходимостью выравнивания переменной uint32_t.

В действительности (после выравнивания структуры, чтобы она могла фактически использовать переменную uint32_t), она может выглядеть примерно так:

struct MyStruct
{
  uint8_t var0;
  uint8_t unused[3];  //3 bytes of wasted space
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

Более эффективной структурой будет:

struct MyStruct
{
  uint8_t var0;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
  uint32_t var1;
};

Теперь вопрос:

Почему компилятор запрещен (стандартным) от переупорядочения структуры?

Я не вижу, чтобы вы могли стрелять себе в ногу, если структура была переупорядочена.

4b9b3361

Ответ 1

Почему компилятор запрещен (стандартным) от переупорядочения структуры?

Основная причина: для совместимости с C.

Помните, что C первоначально является языком ассемблера высокого уровня. В C довольно часто просматривается память (сетевые пакеты,...), переинтерпретируя байты как конкретные struct.

Это привело к нескольким функциям, основанным на этом свойстве:

  • C гарантирует, что адрес struct и адрес его первого элемента данных являются одним и тем же, поэтому С++ тоже (в отсутствии virtual наследования/методов).

  • C гарантировало, что если у вас есть два struct A и B, и оба начинаются с члена данных char, за которым следует член данных int (и все после этого), тогда, когда вы поместите их в union, вы можете написать член B и прочитать char и int через его член A, поэтому С++ тоже: Стандартная компоновка.

Последний является чрезвычайно широким и полностью предотвращает любой переупорядочивание элементов данных для большинства struct (или class).


Обратите внимание, что стандарт разрешает некоторый повторный порядок: поскольку C не имеет концепции контроля доступа, С++ указывает, что относительный порядок двух членов данных с другим спецификатором контроля доступа не указан.

Насколько я знаю, ни один компилятор не пытается его использовать; но они могли бы теоретически.

Вне С++ языки, такие как Rust, явно оставляют за собой право переупорядочивать поля, что наглядно демонстрирует, что это возможно и практично. Только исторические решения и сильное желание обратной совместимости не позволяют С++ делать это.

Ответ 2

Я не вижу, чтобы вы могли стрелять себе в ногу, если структура была переупорядочена.

Действительно? Если это было разрешено, связь между библиотеками/модулями даже в том же процессе по умолчанию была бы смехотворно опасной.

Аргумент "Вселенной"

Мы должны уметь знать, что наши структуры определены так, как мы их просили. Это достаточно плохо, что отступы неуточнены! К счастью, вы можете контролировать это, когда вам нужно.

Хорошо, теоретически, можно было бы создать новый язык, чтобы аналогичным образом члены были повторно упорядочены, если не был дан какой-либо атрибут. В конце концов, мы не должны делать магию уровня памяти на объектах, поэтому, если нужно использовать только идиомы С++, по умолчанию вы будете в безопасности.

Но это не та практическая реальность, в которой мы живем.


Аргумент "Из вселенной"

Вы можете сделать все в порядке, если, по вашим словам, "один и тот же порядок был использован каждый раз". Язык должен был бы однозначно заявить, как будут упорядочены члены. Это сложно написать в стандарте, сложно понять и скомпрометировать.

Намного проще просто гарантировать, что заказ будет таким же, как и в коде, и оставить эти решения программисту. Помните, что эти правила возникли в старом C, а старый C дает программисту возможность.

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

Ответ 3

Стандарт гарантирует порядок распределения просто потому, что структуры могут представлять собой определенный макет памяти, такой как протокол данных или набор аппаратных регистров. Например, ни программист, ни компилятор не могут повторно упорядочить порядок байтов в протоколе TPC/IP или аппаратные регистры микроконтроллера.

Если порядок не был гарантирован, structs был бы простым, абстрактным контейнером данных (похожим на вектор С++), чего мы не можем принять много, за исключением того, что они каким-то образом содержат данные, которые мы помещаем внутри них. Это сделало бы их существенно более бесполезными при выполнении любой формы низкоуровневого программирования.

Ответ 4

Компилятор должен поддерживать порядок своих членов в том случае, если структуры считываются любым другим низкоуровневым кодом, созданным другим компилятором или другим языком. Предположим, вы создавали операционную систему, и вы решили записать ее часть на C и часть ее в сборке. Вы можете определить следующую структуру:

struct keyboard_input
{
    uint8_t modifiers;
    uint32_t scancode;
}

Вы передаете это в процедуру сборки, где вам нужно вручную указать макет памяти структуры. Вы ожидаете, что сможете записать следующий код в системе с 4-байтовым выравниванием.

; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]

Теперь скажем, что компилятор изменит порядок членов в структуре определенным образом, это будет означать, что в зависимости от используемого вами компилятора и флагов, которые вы передаете ему, вы можете либо получить первый байт члена scancode в al или с членом-модификатором.

Конечно, проблема не только сводится к низкоуровневым интерфейсам с процедурами сборки, но также появляется, если библиотеки, построенные с разными компиляторами, будут называть друг друга (например, создание программы с помощью mingw с использованием API окон).

Из-за этого язык просто заставляет вас думать о структуре.

Ответ 5

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

Ответ 6

Вы также цитируете С++, поэтому я дам вам практические причины, почему этого не может быть.

Учитывая нет разницы между class и struct, рассмотрите:

class MyClass
{
    string s;
    anotherObject b;

    MyClass() : s{"hello"}, b{s} 
    {}

};

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

- Затем нестатические элементы данных инициализируются в том порядке, в котором они были объявленный в определении класса

согласно [ base.class.init/13]. Поэтому компилятор не может переупорядочивать поля в определении класса, потому что в противном случае (в качестве примера) члены в зависимости от инициализации других не могли бы работать.

Компилятор строго не требует переупорядочения в памяти (что я могу сказать), но, особенно учитывая вышеприведенный пример, было бы ужасно больно отслеживать это. И я сомневаюсь в каких-либо улучшениях производительности, в отличие от дополнения.

Ответ 7

Язык, разработанный Деннисом Ритчи, определял семантику структур не с точки зрения поведения, а с точки зрения макета памяти. Если структура S имела член M типа T при смещении X, то поведение MS определялось как взятие адреса S, добавление к нему X байтов, интерпретация его как указателя на T и интерпретация хранилища, идентифицированного им как lvalue. Запись элемента структуры изменит содержимое связанного с ним хранилища, а изменение содержимого хранилища элементов изменит значение элемента. Код был свободен для использования самых разных способов управления хранилищем, связанного со структурами, и семантика была бы определена в терминах операций на этом хранилище.

Среди полезных способов, которыми код мог управлять хранилищем, связанным со структурой, было использование memcpy() для копирования произвольной части одной структуры в соответствующую часть другой или memset(), чтобы очистить произвольную часть состав. Поскольку элементы структуры были выложены последовательно, ряд членов можно было скопировать или очистить, используя один вызов memcpy() или memset().

Язык, определенный в стандартном комитете, во многих случаях исключает требование о том, что изменения членов структуры должны влиять на базовое хранилище или что изменения в хранилище влияют на значения членов, что делает гарантии относительно структуры структуры менее полезными, чем они были в Ричи. Тем не менее, возможность использования memcpy() и memset() сохранялась и сохраняла эту способность, требующую сохранения структурных элементов последовательно.

Ответ 8

Предположим, что эта структура структуры фактически представляет собой последовательность памяти, полученную "по проводу", скажем, пакет Ethernet. если компилятор переустанавливает вещи, чтобы быть более эффективными, тогда вам придется выполнять множество работ, вытягивая байты в требуемом порядке, а не просто используя структуру, которая имеет все правильные байты в правильном порядке и месте.