Почему компиляторы 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;
}

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

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

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

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

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

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

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

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

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

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

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

11 ответов


существует несколько причин, по которым компилятор 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 оптимизируют компиляторы, для переупорядочения полей структуры потребуется новая оптимизация. Сомнительно, что эти оптимизации смогут сделать лучше, чем то, что программисты могут написать. Проектирование структур данных вручную гораздо меньше отнимает больше времени, чем другие задачи компилятора, такие как распределение регистров, функция inlining, constant folding, преобразование переключите оператор в двоичный поиск и т. д. Таким образом, преимущества, которые можно получить, позволив компилятору оптимизировать структуры данных, представляются менее ощутимыми, чем традиционные оптимизации компилятора.


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

наблюдайте за этим фактическим кодом с ip-адреса NetBSD.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-дейтаграммы. Оно использован сразу для того чтобы интерпретировать шарики памяти blatted внутри регулятором локальных сетей как дейтаграмма IP заголовки. Представьте себе, если компилятор произвольно перестроит содержимое из-под автора-это будет катастрофа.

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


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

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

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

Не являясь членом WG14, я не могу сказать ничего определенного, но у меня есть свои собственные идеи:

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

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

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


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


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


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

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


имейте в виду, что объявление переменной, например struct, предназначено для "публичного" представления переменной. Он используется не только вашим компилятором, но и доступен другим компиляторам, представляющим этот тип данных. Это, вероятно, закончится в a .H-файл. Поэтому, если компилятор собирается позволить себе вольности с тем, как организованы члены в структуре, все компиляторы должны иметь возможность следовать одним и тем же правилам. В противном случае, как уже упоминалось, указатель арифметика будет путаться между различными компиляторами.


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

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

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


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

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


предположим, у вас есть заголовок a.h с

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

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

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

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

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