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

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

struct dt
{
    unsigned long minute :6;
    unsigned long hour :5;
    unsigned long day :5;
    unsigned long month :4;
    unsigned long year :12;
}stamp;

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

mmmmmm|hhhhh|ddddd|mmmm|yyyyyyyyyyyy

Наконец, предположим, что я просто объявляю unsigned long и разделяю его, используя маски и сдвиги, чтобы делать то же самое.

unsigned long dateTime;

Вот мой вопрос:
Являются ли следующие способы получения минут, часов и т.д. Эквивалентными с точки зрения того, что компьютер должен делать? Или есть какой-то сложный метод, который компилятор/компьютер использует с битовыми полями.

unsigned minutes = stamp.minutes;
//versus
unsigned minutes = ((dateTime & 0xf8000000)>>26;

и

unsigned hours = stamp.hours;
//versus
unsigned hours = ((dateTime & 0x07C00000)>>21;

и др.

Ответ 1

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

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

Ответ 2

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

Быстрое тестирование gcc дает:

shrq    $6, %rdi             ; using bit field
movl    %edi, %eax
andl    $31, %eax

против.

andl    $130023424, %edi     ; by-hand
shrl    $21, %edi
movl    %edi, %eax

Это мало-конечная машина, поэтому числа разные, но три инструкции почти одинаковы.

Ответ 3

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

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

Ответ 4

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

Если ваш dt является целым числом, отформатированным как:

yyyyyyyyyyyy|mmmm|ddddd|hhhhh|mmmmmm

Тогда вы можете естественно сравнить их так.

dt t1(getTimeStamp());
dt t2(getTimeStamp());

if (t1 < t2)
{    std::cout << "T1 happened before T2\n";
}

Используя структуру битового поля, код выглядит следующим образом:

dt t1(getTimeStamp());
dt t2(getTimeStamp());

if (convertToInt(t1) < convertToInt(t2))
{    std::cout << "T1 happened before T2\n";
}
// or
if ((t1.year < t2.year)
    || ((t1.year == t2.year) && ((t1.month < t2.month)
      || ((t1.month == t2.month) && ((t1.day < t2.day)
        || ((t1.day == t2.day) && (t1.hour  etc.....

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

Ответ 5

Только если ваша архитектура явно содержит набор инструкций для побитовой манипуляции и доступа.

Ответ 6

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

  • Он может использовать больше места, чем необходимо, и
  • Доступ к каждому члену будет выполнен эффективно.

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

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

С другой стороны, если вы просто сохраняете все в одном unsigned long (или массиве символов), то вам нужно реализовать эффективный доступ, но у вас есть гарантия макета. Он будет занимать фиксированный размер, и отступов не будет. А это означает, что копирование ценности вокруг может стать менее дорогостоящим. И он будет более переносимым (если вы используете тип int фиксированного размера, а не только unsigned int).

Ответ 7

Компилятор иногда может комбинировать доступ к битовым полям в неинтуитивном вопросе. Я однажды разобрал созданный код (gcc 3.4.6 для sparc) при доступе к 1 битным записям, который используется в условных выражениях. Компилятор подключил доступ к битам и сравнил их с целыми числами. Я попытаюсь воспроизвести идею (не на работе и не могу получить доступ к исходному коду, который был задействован):

struct bits {
  int b1:1;
  int b2:1;
  int b3:1;
  ...
} x;

if(x.b1 && x.b2 && !x.b3)
...
if(x.b2 && !x.b2 && x.b3)

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

temp = (x & 7);
if( temp == 6)
...
if( temp == 5)

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