Побитовая операция приводит к неожиданному размеру переменной

24

контекст

Мы переносим код C, который был изначально скомпилирован с использованием 8-битного компилятора C для микроконтроллера PIC. Обычная идиома, которая использовалась для того, чтобы не допустить повторения нуля беззнаковых глобальных переменных (например, счетчиков ошибок), заключается в следующем:

if(~counter) counter++;

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

проблема

Сейчас мы ориентируемся на 32-битный процессор ARM, используя GCC. Мы заметили, что один и тот же код дает разные результаты. Насколько мы можем судить, похоже, что операция побитового дополнения возвращает значение, которое отличается от ожидаемого. Чтобы воспроизвести это, мы компилируем в GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

В первой строке вывода мы получаем то, что ожидаем: i1 байт. Однако побитовое дополнение iфактически составляет четыре байта, что вызывает проблему, потому что сравнение с этим сейчас не даст ожидаемых результатов. Например, если вы делаете (где iправильно инициализирован uint8_t):

if(~i) i++;

Мы увидим i«обтекание» от 0xFF до 0x00. Это поведение отличается в GCC по сравнению с тем, когда он работал, как мы предполагали в предыдущем компиляторе и 8-битном микроконтроллере PIC.

Мы знаем, что мы можем решить эту проблему следующим образом:

if((uint8_t)~i) i++;

Или

if(i < 0xFF) i++;

Однако в обоих этих обходных путях размер переменной должен быть известен и подвержен ошибкам для разработчика программного обеспечения. Такого рода проверки верхних границ происходят по всей кодовой базе. Существует несколько размеров переменных (например, uint16_tи unsigned charт. Д.), И мы не ожидаем их изменения в другой работающей кодовой базе.

Вопрос

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

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


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

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

Чарли Солтс
источник
14
«Верно ли наше предположение, что операция, такая как побитовое дополнение, должна возвращать результат того же размера, что и операнд?» Нет, это не правильно, применяются целочисленные предложения.
Томас Ягер
2
Хотя это, безусловно, актуально, я не уверен, что это дубликаты этого конкретного вопроса, потому что они не дают решения проблемы.
Коди Грей
3
Я чувствую, что принимаю сумасшедшие таблетки, и С должен быть немного более портативным, чем этот. Если вы не получили целочисленного повышения на 8-битных типах, значит, ваш компилятор не был совместим со стандартом C. В этом случае я думаю, что вы должны пройти все вычисления, чтобы проверить их и исправить в случае необходимости.
user694733
1
Неужели я один задаюсь вопросом, какая логика, кроме действительно неважных счетчиков, может привести ее к «приращению, если места достаточно, иначе забудем об этом»? Если вы переносите код, можете ли вы использовать int (4 байта) вместо uint_8? Это предотвратит вашу проблему во многих случаях.
шайба
1
@puck Вы правы, мы можем изменить его на 4 байта, но это нарушит совместимость при взаимодействии с существующими системами. Намерение состоит в том, чтобы знать, когда есть какие- либо ошибки, и поэтому 1-байтовый счетчик был изначально достаточен и остается таковым.
Чарли Солтс

Ответы:

26

То, что вы видите, является результатом целочисленных рекламных акций . В большинстве случаев, когда в выражении используется целочисленное значение, если тип значения меньше, чем intзначение повышается int. Это задокументировано в разделе 6.3.1.1p2 стандарта C :

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

  • Объект или выражение с целочисленным типом (кроме intили unsigned int), чей ранг целочисленного преобразования меньше или равен рангу intи unsigned int.
  • Битовое поле типа _Bool, со знаком int ,int , orunsigned int`.

Если an intможет представлять все значения исходного типа (как ограничено шириной для битового поля), значение преобразуется в int; в противном случае он преобразуется в unsigned int. Они называются целочисленными акциями . Все остальные типы не изменяются целочисленными акциями.

Таким образом, если переменная имеет тип uint8_tи значение 255, использование любого оператора, кроме преобразования или присвоения, сначала преобразует ее в тип intсо значением 255 перед выполнением операции. Вот почему sizeof(~i)дает вам 4 вместо 1.

Раздел 6.5.3.3 описывает, что целочисленные рекламные акции применяются к ~оператору:

Результатом ~оператора является побитовое дополнение его (повышенного) операнда (то есть каждый бит в результате устанавливается тогда и только тогда, когда соответствующий бит в преобразованном операнде не установлен). Целочисленные продвижения выполняются над операндом, и результат имеет продвинутый тип. Если повышенный тип является типом без знака, выражение ~Eэквивалентно максимальному значению, представляемому в этом типе, минус E.

Таким образом, если принять 32-битное значение int, если оно counterимеет 8-битное значение, 0xffоно преобразуется в 32-битное значение 0x000000ff, и применение ~к нему дает вам 0xffffff00.

Вероятно, самый простой способ справиться с этим, не зная типа, это проверить, равно ли значение 0 после увеличения и, если да, уменьшить его.

if (!++counter) counter--;

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

dbush
источник
1
if (!++counter) --counter;может быть менее странным для некоторых программистов, чем использование оператора запятой.
Эрик Постпишил
1
Еще одна альтернатива ++counter; counter -= !counter;.
Эрик Постпишил
@EricPostpischil На самом деле мне больше нравится ваш первый вариант. Ред.
dbush
15
Это безобразно и нечитаемо, независимо от того, как вы это пишете. Если вам нужно использовать идиому, подобную этой, сделайте услугу каждому программисту по обслуживанию и оберните ее как встроенную функцию : что-то вроде increment_unsigned_without_wraparoundили increment_with_saturation. Лично я бы использовал общую clampфункцию трех операндов .
Коди Грей
5
Кроме того, вы не можете сделать это функцией, потому что она должна вести себя по-разному для разных типов аргументов. Вы должны использовать макрос общего типа .
user2357112 поддерживает Монику
7

в размере (я); Вы запрашиваете размер переменной I , поэтому 1

в размере (~ я); вы запрашиваете размер типа выражения, которое является int , в вашем случае 4


Использовать

если (~ я)

чтобы узнать, если я не значение 255 (в вашем случае с uint8_t) не очень читабельным, просто сделайте

if (i != 255)

и у вас будет портативный и читаемый код


Есть несколько размеров переменных (например, uint16_t и unsigned char и т. Д.)

Для управления любым размером без знака:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

Выражение является константой, поэтому вычисляется во время компиляции.

#include <limit.h> для CHAR_BIT и #include <stdint.h> для uintmax_t

Брюно
источник
3
Вопрос явно утверждает, что они имеют несколько размеров, поэтому != 255неадекватны.
Эрик Постпишил
@EricPostpischil ах, да, я об этом забываю, так что «если (i! = ((1u << sizeof (i) * 8) - 1))» предположим, всегда без знака?
Бруно
1
Это будет неопределенным для unsignedобъектов, поскольку сдвиги полной ширины объекта не определены стандартом C, но это можно исправить с помощью (2u << sizeof(i)*CHAR_BIT-1) - 1.
Эрик Постпишил
о да, конечно, CHAR_BIT, мой плохой
бруно
2
Для безопасности с более широкими типами можно использовать ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Эрик Постпишил
5

Вот несколько вариантов реализации «Добавить 1 к xно зажимать максимальное представимое значение», учитывая, что xэто некоторый целочисленный тип без знака:

  1. Добавьте одно, если и только если xоно меньше максимального значения, представляемого в его типе:

    x += x < Maximum(x);

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

  2. Сравните с наибольшим значением типа:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Это вычисляет 2 N , где N - количество бит в xсмещении 2 на N -1 бит. Мы делаем это вместо смещения 1 N бит, потому что смещение на количество бит в типе не определяется C стандарт. CHAR_BITМакрос может быть незнаком для некоторых, это количество бит в байте, так же sizeof x * CHAR_BITкак и количество бит в типе x.)

    Это можно обернуть в макрос по желанию для эстетики и ясности:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Увеличьте xи исправьте, если оно обнуляется, используя if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Увеличивайте xи корректируйте, если оно обнуляется, используя выражение:

    ++x; x -= !x;

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

  5. Опция без ответвлений, использующая приведенный выше макрос:

    x += 1 - x/Maximum(x);

    Если xэто максимум его типа, это оценивается как x += 1-1. В противном случае это так x += 1-0. Однако на многих архитектурах деление происходит несколько медленно. Компилятор может оптимизировать это до инструкций без деления, в зависимости от компилятора и целевой архитектуры.

Эрик Постпищил
источник
1
Я просто не могу заставить себя ответить на вопрос, который рекомендует использовать макрос. C имеет встроенные функции. В этом макроопределении вы ничего не делаете, что не может быть легко сделано внутри встроенной функции. И если вы собираетесь использовать макрос, убедитесь, что вы используете стратегические скобки для ясности: оператор << имеет очень низкий приоритет. Clang предупреждает об этом с -Wshift-op-parentheses. Хорошая новость заключается в том, что оптимизирующий компилятор не будет генерировать здесь деление, поэтому вам не нужно беспокоиться о том, что оно будет медленным.
Коди Грей
1
@CodyGray, если вы думаете, что можете сделать это с помощью функции, напишите ответ.
Карстен С.
2
@CodyGray: sizeof xне может быть реализован внутри функции C, потому что xэто должен быть параметр (или другое выражение) с фиксированным типом. Это не могло произвести размер любого типа аргумента, который использует вызывающая сторона. Макрос может.
Эрик Постпишил
2

До stdint.h размеры переменных могут варьироваться от компилятора к компилятору, а фактические типы переменных в C по-прежнему int, long и т. Д. И по-прежнему определяются автором компилятора в зависимости от их размера. Не некоторые стандартные и не целевые конкретные предположения. Затем авторам необходимо создать stdint.h для сопоставления двух миров, что является целью stdint.h для сопоставления uint_this с int, long, short.

Если вы портируете код из другого компилятора и он использует char, short, int, long, тогда вам нужно пройтись по каждому типу и выполнить порт самостоятельно, и нет никакого способа обойти это. И если вы получите правильный размер для переменной, объявление изменится, но код, как написано, работает ....

if(~counter) counter++;

или ... поставьте маску или типизацию напрямую

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

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

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

if(sizeof(uint_8)!=1) return(FAIL);

И хотя этот стиль кодирования работает (если (~ counter) counter ++;), для переносимости, желаемой сейчас и в будущем, лучше всего использовать маску, чтобы специально ограничивать размер (а не полагаться на объявление), делайте это, когда Код написан в первую очередь или просто закончите порт, а затем вам не придется переносить его снова в другой день. Или, чтобы сделать код более читабельным, тогда выполните if x <0xFF then или x! = 0xFF или что-то в этом роде, и компилятор может оптимизировать его под тот же код, что и для любого из этих решений, просто делая его более читабельным и менее рискованным. ...

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

Старожил
источник
0
6.5.3.3 Унарные арифметические операторы
...
4 Результатом ~оператора является побитовое дополнение его (повышенного) операнда (то есть каждый бит в результате устанавливается, если и только если соответствующий бит в преобразованном операнде не установлен ). Целочисленные продвижения выполняются над операндом, и результат имеет продвинутый тип . Если повышенный тип является типом без знака, выражение ~Eэквивалентно максимальному значению, представляемому в этом типе, минус E.

C 2011 Онлайн проект

Проблема заключается в том, что операнд ~повышается intдо применения оператора.

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

if ( counter + 1 ) counter++;

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

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;
Джон Боде
источник
Я ценю идею целочисленного продвижения - похоже, это проблема, с которой мы сталкиваемся. Однако стоит отметить, что во втором примере кода -1это не нужно, так как это приведет к тому, что счетчик установится на 254 (0xFE). В любом случае, этот подход, как упоминалось в моем вопросе, не идеален из-за разных размеров переменных в кодовой базе, которые участвуют в этой идиоме.
Чарли Солтс