Почему компиляторы C не могут изменять элементы структуры для исключения выравнивания?
Возможный дубликат:
Почему GCC не оптимизирует структуры? Почему С++ не упрощает структуру?
Рассмотрим следующий пример на 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/С++ не разрешено это делать. Мой вопрос в том, почему язык был разработан таким образом. В конце концов, мы можем в конечном итоге тратить огромные объемы памяти, и ссылки, такие как struct_ref->b
, не будут заботиться об этой разнице.
РЕДАКТИРОВАТЬ: Спасибо всем за ваши чрезвычайно полезные ответы. Вы очень хорошо объясняете, почему перестройка не работает из-за того, как был разработан язык. Тем не менее, это заставляет меня думать: будут ли эти аргументы по-прежнему держаться, если бы перестановка была частью языка? Скажем, что было определенное правило переупорядочения, из которого нам потребовалось хотя бы то, что
- мы должны только реорганизовать структуру, если это действительно необходимо (ничего не делать, если структура уже "плотная" )
- правило просматривает только определение структуры, а не внутренние внутренние структуры. Это гарантирует, что тип структуры имеет один и тот же макет, независимо от того, является ли он внутренним в другой структуре.
- скомпилированный макет памяти данной структуры предсказуем, учитывая его определение (т.е. правило фиксировано)
Адаптация аргументов одна за другой. Причина:
-
Отображение низкоуровневых данных, "элемент наименьшего удивления": просто напишите свои структуры в жестком стиле самостоятельно (например, в ответе @Perry), и ничего не изменилось (требование 1). Если по какой-то странной причине вы хотите, чтобы внутренняя прокладка была там, вы можете вставить ее вручную, используя фиктивные переменные, и/или могут быть ключевые слова/директивы.
-
Различия в компиляторах: Требование 3 устраняет эту проблему. На самом деле, из комментариев @David Heffernan, кажется, что сегодня у нас есть эта проблема, потому что разные компиляторы используют по-разному?
-
Оптимизация. Весь смысл переупорядочения - оптимизация (памяти). Я вижу здесь много возможностей. Возможно, мы не сможем удалить прописку вместе, но я не вижу, как переупорядочение может каким-либо образом ограничить оптимизацию.
-
Тип кастинга: Мне кажется, что это самая большая проблема. Тем не менее, должны быть пути вокруг этого. Поскольку правила фиксированы на языке, компилятор может выяснить, как члены были переупорядочены, и реагировать соответственно. Как упоминалось выше, всегда будет возможно предотвратить переупорядочение в тех случаях, когда вы хотите получить полный контроль. Кроме того, требование 2 гарантирует, что безопасный код никогда не сломается.
Причина, по которой я думаю, что такое правило может иметь смысл, состоит в том, что я считаю более естественным группировать элементы структуры по их содержанию, чем по их типам. Кроме того, компилятор легче выбрать лучший порядок, чем для меня, когда у меня много внутренних структур. Оптимальный макет может даже быть тем, который я не могу выразить безопасным типом. С другой стороны, казалось бы, язык стал более сложным, что, конечно, является недостатком.
Обратите внимание, что я не говорю об изменении языка - только если он (/должен) был разработан по-разному.
Я знаю, что мой вопрос гипотетический, но я думаю, что обсуждение дает более глубокое понимание нижних уровней машинного и языкового дизайна.
Я здесь совершенно новый, поэтому я не знаю, должен ли я задать новый вопрос для этого. Скажите, пожалуйста, если это так.
Ответы
Ответ 1
Существует несколько причин, по которым компилятор C не может автоматически изменить порядок полей:
-
Компилятор C не знает, отражает ли struct
структуру памяти объектов за пределами текущего модуля компиляции (например: внешняя библиотека, файл на диске, сетевые данные, таблицы страниц ЦП...). В этом случае двоичная структура данных также определяется в недоступном компилятору месте, поэтому переупорядочение полей 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 в двоичный поиск и т.д. Таким образом, преимущества, которые необходимо получить, позволяя компилятору оптимизировать структуры данных кажутся менее ощутимыми, чем традиционные оптимизации компилятора.
Ответ 2
C разработан и предназначен для того, чтобы можно было писать непереносимый аппаратный и зависимый от формата код на языке высокого уровня. Перегруппировка содержимого структуры за спиной программиста уничтожит эту способность.
Соблюдайте этот фактический код из NetBSD ip.h:
/*
* 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-дейтаграммы. Он используется для прямой интерпретации блоб памяти, которые впитывает контроллер ethernet в виде заголовков IP-дейтаграмм. Представьте себе, если компилятор произвольно переустановил содержимое из-под автора - это было бы катастрофой.
И да, это не совсем портативно (и там даже не переносная директива gcc, указанная там через макрос __packed
), но это не так. C специально разработан, чтобы сделать возможным запись непортативного кода высокого уровня для управления оборудованием. Это его функция в жизни.
Ответ 3
C [и С++] рассматриваются как языки системного программирования, поэтому обеспечивают низкоуровневый доступ к аппаратным средствам, например, памяти с помощью указателей. Программист может получить доступ к блоку данных и передать его в структуру и получить доступ к различным членам [легко].
Другим примером является структура, подобная приведенной ниже, в которой хранятся данные с переменным размером.
struct {
uint32_t data_size;
uint8_t data[1]; // this has to be the last member
} _vv_a;
Ответ 4
Не будучи членом WG14, я не могу сказать ничего окончательного, но у меня есть свои собственные идеи:
-
Это нарушит принцип наименьшего удивления - может быть, есть чертовски веская причина, по которой я хочу выложить свои элементы в определенном порядке, независимо от того, является ли она самой эффективной с точки зрения пространства, и я бы не хотите, чтобы компилятор переставлял эти элементы;
-
Он может разбить нетривиальную сумму существующего кода - там много устаревшего кода, который полагается на такие вещи, как адрес структуры, такой же, как адрес первого члена ( видел много классического кода MacOS, который сделал это предположение);
C99 Обоснование напрямую адресует второй пункт ( "Существующий код важен, существующие реализации не являются" ) и косвенно обращается к первому ( "Доверяйте программисту" ).
Ответ 5
Это изменит семантику операций указателя, чтобы изменить порядок элементов структуры. Если вы заботитесь о компактном представлении памяти, это ваша ответственность как программист знать вашу целевую архитектуру и организовать ваши структуры соответственно.
Ответ 6
Если вы читали/записывали двоичные данные в/из структур C, переупорядочение членов struct
было бы катастрофой. Например, практического способа фактического заполнения структуры из буфера не было.
Ответ 7
Структуры используются для представления физического оборудования на самых низких уровнях. Таким образом, компилятор не может перемещать вещи на круги своя.
Однако было бы неразумно иметь #pragma, позволяющую компилятору переупорядочивать структуры, основанные на памяти, которые используются только внутри программы. Однако я не знаю такого зверя (но это не значит, что я сидел на корточках - я не знаком с C/С++)
Ответ 8
Имейте в виду, что объявление переменной, такое как структура, должно быть "общедоступным" представлением переменной. Он используется не только вашим компилятором, но также доступен для других компиляторов, представляющих этот тип данных. Вероятно, это будет в файле .h. Поэтому, если компилятор будет принимать вольности с тем, как организованы члены внутри структуры, тогда ВСЕ компиляторы должны иметь возможность следовать тем же правилам. В противном случае, как уже упоминалось, арифметика указателя путается между разными компиляторами.
Ответ 9
Вот причина, по которой я не видел до сих пор - без стандартных правил переупорядочения, это нарушит совместимость исходных файлов.
Предположим, что структура определена в файле заголовка и используется в двух файлах.
Оба файла скомпилированы отдельно и позже связаны. Компиляция может быть в разное время (возможно, вы коснулись только одного, поэтому ее пришлось перекомпилировать), возможно, на разных компьютерах (если файлы находятся на сетевом диске) или даже в разных версиях компилятора.
Если в свое время компилятор решил изменить порядок, а в другом - нет, два файла не будут согласовываться с полями.
В качестве примера рассмотрим системный вызов stat
и struct stat
.
Когда вы устанавливаете Linux (например), вы получаете libC, который включает stat
, который когда-то был скомпилирован кем-то.
Затем вы скомпилируете приложение с вашим компилятором с помощью флагов оптимизации и ожидаете согласования структуры структуры.
Ответ 10
Ваш случай очень специфичен, так как требуется, чтобы первый элемент struct
был переупорядочен. Это невозможно, так как элемент, который сначала определяется в struct
, всегда должен быть смещен 0
. Много (фиктивный) код сломается, если это будет разрешено.
Более общие указатели подобъектов, которые живут внутри одного и того же более крупного объекта, всегда должны учитывать сравнение указателей. Я могу себе представить, что какой-то код, который использует эту функцию, сломается, если вы измените порядок. И для этого сравнения знание компилятора в точке определения не помогло бы: указатель на подобъект не имеет "отметки", какой крупный объект он принадлежит. Когда передается другая функция как таковая, вся информация о возможном контексте теряется.
Ответ 11
предположим, что у вас есть заголовок a.h с
struct s1 {
char a;
int b;
char c;
char d;
char e;
}
и это часть отдельной библиотеки (у вас есть только скомпилированные двоичные файлы, скомпилированные неизвестным компилятором), и вы хотите использовать эту структуру для связи с этой библиотекой,
если компилятору разрешено изменять порядок членов в зависимости от того, каким образом ему нравится , это будет невозможно, поскольку компилятор клиента не знает, использовать ли структуру как-либо или оптимизировано (а затем b
идти спереди или сзади) или даже полностью дополнять каждый элемент, выровненный по 4-байтным интервалам
чтобы решить эту проблему, вы можете определить детерминированный алгоритм для уплотнения, но для этого требуется, чтобы все компиляторы реализовали его и что алгоритм является хорошим (с точки зрения эффективности). проще просто согласовать правила заполнения, чем при переупорядочивании
легко добавить #pragma
, который запрещает оптимизацию, когда вам нужен макет для конкретной структуры, именно то, что вам нужно, чтобы не было проблем