Побитовая операция приводит к неожиданному размеру переменной - PullRequest
24 голосов
/ 15 апреля 2020

Контекст

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

if(~counter) counter++;

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

Проблема

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

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

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

if(~i) i++;

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

Мы знаем, что можем решить эту проблему путем приведения типа Итак:

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

Или

if(i < 0xFF) i++;

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

Вопрос

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

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


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

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

Ответы [ 5 ]

26 голосов
/ 15 апреля 2020

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

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

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

Если 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 после увеличения, и если да, уменьшить его. положительное значение.

7 голосов
/ 15 апреля 2020

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

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


Для использования

if (~ i)

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

if (i != 255)

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


. Имеются переменные разных размеров (например, uint16_t и unsigned char et c.)

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

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

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

# include для CHAR_BIT и # включают для uintmax_t

5 голосов
/ 15 апреля 2020

Вот несколько вариантов реализации «Добавить 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. Однако на многих архитектурах деление происходит несколько медленно. Компилятор может оптимизировать это до инструкций без деления, в зависимости от компилятора и целевой архитектуры.

2 голосов
/ 15 апреля 2020

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

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

if(~counter) counter++;

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

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

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

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

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

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

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

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

C 2011 Онлайн-черновик

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

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

if ( counter + 1 ) counter++;

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

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;
...