Почему компиляторы C не могут изменять элементы структуры для исключения выравнивания?

Возможный дубликат:
Почему GCC не оптимизирует структуры?
Почему C ++ не упрощает структуру?

Рассмотрим следующий пример на 32-битной машине x86:

Из-за ограничений выравнивания, следующая структура

struct s1 { char a; int b; char c; char d; char e; } 

могут быть представлены более эффективно с точки зрения памяти (12 против 8 байтов), если члены были переупорядочены, как в

 struct s2 { int b; char a; char c; char d; char e; } 

Я знаю, что компиляторы C / C ++ не могут это делать. Мой вопрос в том, почему язык был разработан таким образом. В конце концов, мы можем в конечном итоге тратить огромные объемы памяти, и ссылки, такие как struct_ref->b , не волнуют разницу.

EDIT : Спасибо всем за ваши чрезвычайно полезные ответы. Вы очень хорошо объясняете, почему перестройка не работает из-за того, как был разработан язык. Тем не менее, это заставляет меня думать: будут ли эти аргументы по-прежнему держаться, если бы перестановка была частью языка? Предположим, что существует определенное правило переупорядочения, из которого мы требовали по крайней мере того, что

  1. мы должны только реорганизовать структуру, если это действительно необходимо (не делайте ничего, если структура уже «плотная»)
  2. правило только смотрит на определение структуры, а не внутри внутренних структур. Это гарантирует, что тип структуры имеет тот же макет, независимо от того, является ли он внутренним в другой структуре
  3. скомпилированный макет памяти данной структуры предсказуем, учитывая его определение (то есть правило фиксировано)

Подчеркивая аргументы один за другим, я рассуждаю:

  • Низкоуровневое сопоставление данных, «элемент наименьшего удивления» : просто напишите свои структуры в жестком стиле самостоятельно (как в ответе @ Перри), и ничего не изменилось (требование 1). Если по какой-то странной причине вы хотите, чтобы внутри было добавлено внутреннее дополнение, вы можете вставить его вручную, используя фиктивные переменные, и / или могут быть ключевые слова / директивы.

  • Различия в компиляторах : Требование 3 устраняет эту проблему. Собственно, из комментариев @David Heffernan, кажется, что сегодня у нас есть эта проблема, потому что разные компиляторы используют по-разному?

  • Оптимизация . Весь смысл переупорядочения – оптимизация (памяти). Я вижу здесь много возможностей. Возможно, мы не сможем удалить прописку вместе, но я не вижу, как переупорядочение может каким-либо образом ограничить оптимизацию.

  • Тип кастинга : Мне кажется, что это самая большая проблема. Тем не менее, должны быть пути вокруг этого. Поскольку правила фиксированы на языке, компилятор может выяснить, как члены были переупорядочены, и реагировать соответственно. Как упоминалось выше, всегда будет возможно предотвратить переупорядочение в тех случаях, когда вы хотите получить полный контроль. Кроме того, требование 2 гарантирует, что безопасный код никогда не сломается.

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

Обратите внимание, что я не говорю об изменении языка – только если он (/ должен) был разработан по-разному.

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

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

Существует несколько причин, по которым компилятор C не может автоматически изменить порядок полей:

  • Компилятор C не знает, представляет ли struct структуры объектов вне текущего модуля компиляции (например: внешняя библиотека, файл на диске, сетевые данные, таблицы страниц CPU, …). В этом случае двоичная структура данных также определяется в месте, недоступном компилятору, поэтому переупорядочение полей struct создало бы тип данных, который несовместим с другими определениями. Например, заголовок файла в ZIP-файле содержит несколько несогласованных 32-битных полей. Переупорядочение полей сделало бы невозможным, чтобы код C напрямую читал или записывал заголовок (предполагая, что реализация ZIP хотела бы получить доступ к данным напрямую):

     struct __attribute__((__packed__)) LocalFileHeader { uint32_t signature; uint16_t minVersion, flag, method, modTime, modDate; uint32_t crc32, compressedSize, uncompressedSize; uint16_t nameLength, extraLength; }; 

    packed атрибут не позволяет компилятору выравнивать поля в соответствии с их естественным выравниванием и не имеет никакого отношения к проблеме упорядочения полей. Можно было бы изменить порядок полей LocalFileHeader чтобы структура имела как минимальный размер, так и все поля, соответствующие их естественному выравниванию. Однако компилятор не может изменить порядок полей, потому что он не знает, что структура фактически определяется спецификацией файла ZIP.

  • C – небезопасный язык. Компилятор C не знает, будут ли доступ к данным через другой тип, чем тот, который видит компилятор, например:

     struct S { char a; int b; char c; }; struct S_head { char a; }; struct S_ext { char a; int b; char c; int d; char e; }; struct S s; struct S_head *head = (struct S_head*)&s; fn1(head); struct S_ext ext; struct S *sp = (struct S*)&ext; fn2(sp); 

    Это широко используемый низкоуровневый шаблон программирования, особенно если заголовок содержит идентификатор типа данных, расположенный непосредственно за заголовком.

  • Если тип struct встроен в другой тип struct , невозможно встроить внутреннюю struct :

     struct S { char a; int b; char c, d, e; }; struct T { char a; struct S s; // Cannot inline S into T, 's' has to be compact in memory char b; }; 

    Это также означает, что перемещение некоторых полей из S в отдельную структуру отключает некоторые оптимизации:

     // Cannot fully optimize S struct BC { int b; char c; }; struct S { char a; struct BC bc; char d, e; }; 
  • Поскольку большинство компиляторов C оптимизируют компиляторы, для переупорядочения структурных полей потребуются новые оптимизации. Не вызывает сомнений, смогут ли эти оптимизации сделать лучше, чем программисты могут писать. Проектирование структур данных вручную намного меньше, чем другие задачи компилятора, такие как распределение регистров, встраивание функций, постоянная сворачивание, преобразование оператора switch в двоичный поиск и т. Д. Таким образом, преимущества, которые необходимо получить, позволяя компилятору оптимизировать структуры данных кажутся менее ощутимыми, чем традиционные оптимизации компилятора.

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

Соблюдайте этот фактический код из ip.h NetBSD:

 /* * Structure of an internet header, naked of options. */ struct ip { #if BYTE_ORDER == LITTLE_ENDIAN unsigned int ip_hl:4, /* header length */ ip_v:4; /* version */ #endif #if BYTE_ORDER == BIG_ENDIAN unsigned int ip_v:4, /* version */ ip_hl:4; /* header length */ #endif u_int8_t ip_tos; /* type of service */ u_int16_t ip_len; /* total length */ u_int16_t ip_id; /* identification */ u_int16_t ip_off; /* fragment offset field */ u_int8_t ip_ttl; /* time to live */ u_int8_t ip_p; /* protocol */ u_int16_t ip_sum; /* checksum */ struct in_addr ip_src, ip_dst; /* source and dest address */ } __packed; 

Эта структура идентична структуре в заголовке IP-дейтаграммы. Он используется для прямой интерпретации блоб памяти, которые впитывает controller ethernet в виде заголовков IP-дейтаграмм. Представьте себе, если компилятор произвольно перестроил содержимое из-под автора – это было бы катастрофой.

И да, это не совсем портативно (и там есть даже переносная gcc-директива, предоставленная там с помощью макроса __packed ), но это не __packed . C специально разработан, чтобы сделать возможным запись непортативного кода высокого уровня для управления оборудованием. Это его функция в жизни.

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

Другим примером является структура, подобная приведенной ниже, в которой хранятся данные с переменным размером.

 struct { uint32_t data_size; uint8_t data[1]; // this has to be the last member } _vv_a; 

Не будучи членом РГ14, я не могу сказать ничего окончательного, но у меня есть свои собственные идеи:

  1. Это нарушит принцип наименьшего удивления – может быть, есть проклятая веская причина, по которой я хочу выложить свои элементы в определенном порядке, независимо от того, является ли она самой эффективной с точки зрения пространства, и я бы не хотел, чтобы компилятор перестраивал эти элементы;

  2. У этого есть потенциал, чтобы сломать нетривиальное количество существующего кода – там много устаревшего кода, который полагается на такие вещи, как адрес структуры, такой же, как адрес первого члена (видел много classического MacOS код, который сделал это предположение);

Обоснование C99 напрямую адресует второй пункт («Существующий код важен, существующие реализации не являются») и косвенно обращается к первому («Доверяйте программисту»).

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

Если вы читали / записывали двоичные данные в / из структур C, переупорядочение членов struct было бы катастрофой. Например, практического способа фактического заполнения структуры из буфера не существует.

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

Однако было бы неразумно иметь #pragma, позволяющую компилятору переупорядочивать структуры на основе памяти, которые используются только внутри программы. Однако я не знаю такого зверя (но это не означает приседания – я не знаком с C / C ++)

Имейте в виду, что объявление переменной, например struct, должно быть «общедоступным» представлением переменной. Он используется не только вашим компилятором, но также доступен для других компиляторов, представляющих этот тип данных. Вероятно, это будет в файле .h. Поэтому, если компилятор будет принимать вольности с тем, как организованы члены внутри структуры, тогда ВСЕ компиляторы должны иметь возможность следовать тем же правилам. В противном случае, как уже упоминалось, арифметика указателя путается между разными компиляторами.

Вот почему я не видел до сих пор – без стандартных правил переупорядочения он нарушил бы совместимость исходных файлов.

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

В качестве примера рассмотрим системный вызов struct stat и struct stat .
Когда вы устанавливаете Linux (например), вы получаете libC, который включает stat , который когда-то был скомпилирован кем-то.
Затем вы скомпилируете приложение с вашим компилятором с помощью флагов оптимизации и ожидаете, что оба согласятся на макет структуры.

Ваш случай очень специфичен, так как требуется, чтобы первый элемент struct был перенастроен. Это невозможно, поскольку элемент, который сначала определяется в struct всегда должен быть смещен 0 . Много (фиктивный) код сломается, если это будет разрешено.

Более общие указатели подобъектов, которые живут внутри одного и того же более крупного объекта, всегда должны допускать сравнение указателей. Я могу себе представить, что какой-то код, который использует эту функцию, сломается, если вы измените порядок. И для этого сравнения знание компилятора в точке определения не помогло бы: указатель на подобъект не имеет «отметки», какой крупный объект он принадлежит. Когда передается другая функция как таковая, вся информация о возможном контексте теряется.

предположим, что у вас есть заголовок ах с

 struct s1 { char a; int b; char c; char d; char e; } 

и это часть отдельной библиотеки (из которой у вас есть только скомпилированные двоичные файлы, скомпилированные неизвестным компилятором), и вы хотите использовать эту структуру для связи с этой библиотекой,

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

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

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

  • Вызывать функцию golang struct дает «не может ссылаться на невыполненное поле или метод»
  • Разница между 'struct' и 'typedef struct' в C ++?
  • Каковы различия между структурой и classом в C ++?
  • Изменить переменную Struct в словаре
  • Структурировать объекты в Java
  • C Typedef и Struct Question
  • Когда вы должны использовать class vs struct в C ++?
  • как устанавливать и получать поля в структурах Golang?
  • Структура массивов по сравнению с массивом структур в CUDA
  • Почему я не могу скопировать инициализацию структуры, полученной из другой структуры?
  • Как правильно назначить новое строковое значение?
  • Давайте будем гением компьютера.