Неточный размер структуры с битовыми полями в моей книге

Я изучаю основы языка C. Я пришел к главе структур с битовыми полями. В книге показан пример структуры с двумя различными типами данных: различными bools и различными unsigned ints.

Книга заявляет, что структура имеет размер 16 бит и что без использования прокладки структура будет измерять 10 бит.

Это структура, которую книга использует в примере:

#include <stdio.h>
#include <stdbool.h>

struct test{

       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
};

int main(void)
{
       struct test Test;

       printf("%zu\n", sizeof(Test));

       return 0;
}

Почему на моем компиляторе вместо этого точно такая же структура измеряет 16 байтов (а не бит) с заполнением и 16 байтов без заполнения?

я использую

GCC (tdm-1) 4.9.2 compiler;
Code::Blocks as IDE.
Windows 7 64 Bit
Intel CPU 64 bit

Это результат, который я получаю:

enter image description here

Вот пример страницы, на которой показан пример:

enter image description here

Ответ 1

Microsoft ABI устанавливает битподы по-другому, чем GCC обычно делает это на других платформах. Вы можете использовать Microsoft-совместимый макет с параметром -mms-bitfields или отключить его с помощью -mno-ms-bitfields. Вероятно, ваша версия GCC по умолчанию использует -mms-bitfields.

Согласно документации, когда -mms-bitfields:

  • Каждый объект данных имеет требование выравнивания. Требование выравнивания для всех данных, кроме структур, объединений и массивов, - это либо размер объекта, либо текущий размер упаковки (указанный либо с выровненным атрибутом, либо с пакетом pragma), в зависимости от того, что меньше. Для структур, союзов и массивов требование выравнивания является самым большим требованием к выравниванию его членов. Каждому объекту присваивается смещение так, чтобы: offset% alignment_requirement == 0
  • Смежные битовые поля упаковываются в те же 1-, 2- или 4-байтовые единицы выделения, если интегральные типы имеют одинаковый размер, и если следующее поле бит вписывается в текущую единицу выделения без пересечения границы, наложенной общие требования к выравниванию бит-полей.

Поскольку bool и unsigned int имеют разные размеры, они упаковываются и выравниваются отдельно, что существенно увеличивает размер структуры. Выравнивание unsigned int составляет 4 байта, а трижды перестраиваться в середине структуры приводит к общему размеру 16 байт.

Вы можете получить такое же поведение в книге, изменив bool на unsigned int или указав -mno-ms-bitfields (хотя это будет означать, что вы не можете взаимодействовать с кодом, скомпилированным на компиляторах Microsoft).

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

Ответ 2

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

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

(C2011, 6.7.2.1/11)

Текст также содержит предположения о заполнении, которые не поддерживаются стандартом. Внедрение AC может включать произвольное количество дополнений после любых или всех элементов и блоков хранения битполя struct, включая последнюю. Реализации обычно используют эту свободу для рассмотрения соображений согласования данных, но C не ограничивает дополнение к использованию. Это полностью отделено от безымянных битовых полей, которые ваш текст называет "заполнением".

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

Почему на моем компиляторе вместо этого точно такая же структура измеряет 16 байтов (а не бит) с заполнением и 16 байтов без заполнения?

Чтобы вырезать текст настолько сильно, насколько это возможно, он различает количество бит данных, занятых членами (всего 16 бит, 6, принадлежащих неназванным битовым полям) и общий размер экземпляров struct. Кажется, что утверждается, что общая структура будет размером unsigned int, который, по-видимому, составляет 32 бита в описываемой системе, и это будет одинаково для обеих версий структуры.

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

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

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

Ответ 3

К моему удивлению, похоже, что разница между некоторыми компиляторами GCC 4.9.2. Во-первых, это мой код:

#include <stdio.h>
#include <stdbool.h>

struct test {
       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
};

struct test_packed {
       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
} __attribute__((packed));

int main(void)
{
       struct test padding;
       struct test_packed no_padding;

       printf("with padding: %zu bytes = %zu bits\n", sizeof(padding), sizeof(padding) * 8);
       printf("without padding: %zu bytes = %zu bits\n", sizeof(no_padding), sizeof(no_padding) * 8);

       return 0;
}

И теперь, результаты от разных компиляторов.

GCC 4.9.2 от WandBox:

with padding: 4 bytes = 32 bits
without padding: 2 bytes = 16 bits

GCC 4.9.2 из http://cpp.sh/:

with padding: 4 bytes = 32 bits
without padding: 2 bytes = 16 bits

НО

GCC 4.9.2 от theonlinecompiler.com:

with padding: 16 bytes = 128 bits
without padding: 16 bytes = 128 bits

(для правильной компиляции вам нужно chagne %zu до %u)

РЕДАКТИРОВАТЬ

@interjay ответ может объяснить это. Когда я добавил -mms-bitfields в GCC 4.9.2 из WandBox, я получил следующее:

with padding: 16 bytes = 128 bits
without padding: 16 bytes = 128 bits

Ответ 4

В стандарте C не описываются все подробности о том, как переменные должны быть помещены в память. Это оставляет место для оптимизации, которая зависит от используемой платформы.

Чтобы дать себе представление о том, как вещи находятся в памяти, вы можете сделать вот так:

#include <stdio.h>
#include <stdbool.h>

struct test{

       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
};


int main(void)
{
  struct test Test = {0};
  int i;
  printf("%zu\n", sizeof(Test));

  unsigned char* p;
  p = (unsigned char*)&Test;
  for(i=0; i<sizeof(Test); ++i)
  {
    printf("%02x", *p);
    ++p;
  }
  printf("\n");

  Test.opaque = true;

  p = (unsigned char*)&Test;
  for(i=0; i<sizeof(Test); ++i)
  {
    printf("%02x", *p);
    ++p;
  }
  printf("\n");

  Test.fill_color = 3;

  p = (unsigned char*)&Test;
  for(i=0; i<sizeof(Test); ++i)
  {
    printf("%02x", *p);
    ++p;
  }
  printf("\n");

  return 0;
}

Выполняя это на ideone (https://ideone.com/wbR5tI), я получаю:

4
00000000
01000000
07000000

Поэтому я вижу, что opaque и fill_color находятся в первом байте. Выполнение точно такого же кода на компьютере Windows (с использованием gcc) дает:

16
00000000000000000000000000000000
01000000000000000000000000000000
01000000030000000000000000000000

Поэтому здесь я вижу, что opaque и fill_color не оба в первом байте. Кажется, что opaque занимает 4 байта.

Это объясняет, что вы получаете всего 16 байтов, то есть bool занимает 4 байта, а затем 4 байта для полей между ними и после.

Ответ 5

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

Для этого он добавляет (вставляет) несколько неиспользуемых битов (битполе):

unsigned int       4;  // padding of the first byte

он также добавляет второй байт, но в этом нет необходимости.

Таким образом, перед заполнением было бы 10 бит в использовании, и, кроме того, для заполнения было определено 16 бит (но не все из них используются).


Примечание: автор использует bool для указания поля 1/0. Далее автор предполагает, что тип _Bool C99 псевдонимом bool. Но, похоже, компиляторы здесь немного запутались. Замена bool на unsigned int решит его. От C99:

6.3.2. Следующие выражения могут использоваться в выражении везде, где может использоваться int или unsigned int:

  • Битовое поле типа _Bool, int, signed int или unsigned int.

Ответ 6

Вы совершенно неверно истолковываете то, что говорит книга.

Объявлено 16 бит бит бит. 6 бит - это неназванные поля, которые нельзя использовать ни для чего, - что упомянутое дополнение. 16 бит минус 6 бит равно 10 бит. Не считая полей заполнения, структура имеет 10 полезных бит.

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

Ответ 7

Исторически сложилось два общих способа интерпретации типов элементов битового поля:

  1. Проверьте, подписан ли тип или нет, но игнорируйте различия между "char", "short", "int" и т.д. При выборе места для размещения элемента.

  2. Если битовому полю предшествует другое с тем же типом или соответствующий тип signed/unsigned, выделите объект такого типа и поместите в него битполе. Поместите следующие битовые поля с тем же типом в этот объект, если они подходят.

Я думаю, что мотивация № 2 заключалась в том, что на платформе, где 16-битные значения должны быть выровнены по словам, компилятор дал что-то вроде:

struct foo {
  char x;  // Not a bitfield
  someType f1 : 1;
  someType f2 : 7;
};

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

struct foo {
  char x;  // Not a bitfield
  someType f1 : 1;
  someType f2 : 15;
};

было бы необходимо, чтобы все f2 соответствовали одному 16-битовому слову, что, таким образом, потребовало бы байта заполнения перед f1. Из-за правила Common Initial Sequence f1 должно быть размещено идентично в этих двух структурах, что подразумевает, что если f1 может удовлетворять правилу Common Initial Sequence, ему потребуется заполнить его до его даже в первой структуре.

Как бы то ни было, код, который хочет разрешить более плотную компоновку в первом случае, может сказать:

struct foo {
  char x;  // Not a bitfield
  unsigned char f1 : 1;
  unsigned char f2 : 7;
};

и предложите компилятору поместить оба битовых поля в байты сразу после x. Поскольку тип указан как unsigned char, компилятору не нужно беспокоиться о возможности 15-битного поля. Если макет был:

struct foo {
  char x;  // Not a bitfield
  unsigned short f1 : 1;
  unsigned short f2 : 7;
};

и намерение состояло в том, что f1 и f2 будут размещаться в одном хранилище, тогда компилятору необходимо будет разместить f1 таким образом, чтобы он мог поддерживать выровненный по словам доступ для своего "bunkmate" f2. Если код был:

struct foo {
  char x;  // Not a bitfield
  unsigned char f1 : 1;
  unsigned short f2 : 15;
};

то f1 будет помещаться вслед за x и f2 в одно слово.

Обратите внимание, что в стандарте C89 добавлен синтаксис, чтобы заставить макет помешать f1 помещаться в байт до использования хранилища f2:

struct foo {
  char x;  // Not a bitfield
  unsigned short : 0;  // Forces next bitfield to start at a "short" boundary
  unsigned short f1 : 1;
  unsigned short f2 : 15;
};

Добавление синтаксиса: 0 в C89 во многом исключает необходимость компилятора рассматривать изменяющиеся типы как принудительное выравнивание, за исключением обработки старого кода.