контекст
Мы переносим код 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
В первой строке вывода мы получаем то, что ожидаем: i
1 байт. Однако побитовое дополнение 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-битный результат
Я не видел реальной сути обсуждаемой там проблемы, а именно, размер результата побитового дополнения отличается от того, что передается оператору.
источник
Ответы:
То, что вы видите, является результатом целочисленных рекламных акций . В большинстве случаев, когда в выражении используется целочисленное значение, если тип значения меньше, чем
int
значение повышаетсяint
. Это задокументировано в разделе 6.3.1.1p2 стандарта C :Таким образом, если переменная имеет тип
uint8_t
и значение 255, использование любого оператора, кроме преобразования или присвоения, сначала преобразует ее в типint
со значением 255 перед выполнением операции. Вот почемуsizeof(~i)
дает вам 4 вместо 1.Раздел 6.5.3.3 описывает, что целочисленные рекламные акции применяются к
~
оператору:Таким образом, если принять 32-битное значение
int
, если оноcounter
имеет 8-битное значение,0xff
оно преобразуется в 32-битное значение0x000000ff
, и применение~
к нему дает вам0xffffff00
.Вероятно, самый простой способ справиться с этим, не зная типа, это проверить, равно ли значение 0 после увеличения и, если да, уменьшить его.
Оборачивание целых чисел без знака работает в обоих направлениях, поэтому уменьшение значения на 0 дает вам наибольшее положительное значение.
источник
if (!++counter) --counter;
может быть менее странным для некоторых программистов, чем использование оператора запятой.++counter; counter -= !counter;
.increment_unsigned_without_wraparound
илиincrement_with_saturation
. Лично я бы использовал общуюclamp
функцию трех операндов .в размере (я); Вы запрашиваете размер переменной I , поэтому 1
в размере (~ я); вы запрашиваете размер типа выражения, которое является int , в вашем случае 4
Использовать
чтобы узнать, если я не значение 255 (в вашем случае с uint8_t) не очень читабельным, просто сделайте
и у вас будет портативный и читаемый код
Для управления любым размером без знака:
Выражение является константой, поэтому вычисляется во время компиляции.
#include <limit.h> для CHAR_BIT и #include <stdint.h> для uintmax_t
источник
!= 255
неадекватны.unsigned
объектов, поскольку сдвиги полной ширины объекта не определены стандартом C, но это можно исправить с помощью(2u << sizeof(i)*CHAR_BIT-1) - 1
.((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
.Вот несколько вариантов реализации «Добавить 1 к
x
но зажимать максимальное представимое значение», учитывая, чтоx
это некоторый целочисленный тип без знака:Добавьте одно, если и только если
x
оно меньше максимального значения, представляемого в его типе:См. Следующий пункт для определения
Maximum
. Этот метод имеет хорошие шансы быть оптимизированным компилятором под эффективные инструкции, такие как сравнение, некоторая форма условного набора или перемещения и добавление.Сравните с наибольшим значением типа:
(Это вычисляет 2 N , где N - количество бит в
x
смещении 2 на N -1 бит. Мы делаем это вместо смещения 1 N бит, потому что смещение на количество бит в типе не определяется C стандарт.CHAR_BIT
Макрос может быть незнаком для некоторых, это количество бит в байте, так жеsizeof x * CHAR_BIT
как и количество бит в типеx
.)Это можно обернуть в макрос по желанию для эстетики и ясности:
Увеличьте
x
и исправьте, если оно обнуляется, используяif
:Увеличивайте
x
и корректируйте, если оно обнуляется, используя выражение:Это номинально без ветвления (иногда полезно для производительности), но компилятор может реализовать его так же, как описано выше, используя ветку при необходимости, но, возможно, с безусловными инструкциями, если целевая архитектура имеет подходящие инструкции.
Опция без ответвлений, использующая приведенный выше макрос:
Если
x
это максимум его типа, это оценивается какx += 1-1
. В противном случае это такx += 1-0
. Однако на многих архитектурах деление происходит несколько медленно. Компилятор может оптимизировать это до инструкций без деления, в зависимости от компилятора и целевой архитектуры.источник
-Wshift-op-parentheses
. Хорошая новость заключается в том, что оптимизирующий компилятор не будет генерировать здесь деление, поэтому вам не нужно беспокоиться о том, что оно будет медленным.sizeof x
не может быть реализован внутри функции C, потому чтоx
это должен быть параметр (или другое выражение) с фиксированным типом. Это не могло произвести размер любого типа аргумента, который использует вызывающая сторона. Макрос может.До stdint.h размеры переменных могут варьироваться от компилятора к компилятору, а фактические типы переменных в C по-прежнему int, long и т. Д. И по-прежнему определяются автором компилятора в зависимости от их размера. Не некоторые стандартные и не целевые конкретные предположения. Затем авторам необходимо создать stdint.h для сопоставления двух миров, что является целью stdint.h для сопоставления uint_this с int, long, short.
Если вы портируете код из другого компилятора и он использует char, short, int, long, тогда вам нужно пройтись по каждому типу и выполнить порт самостоятельно, и нет никакого способа обойти это. И если вы получите правильный размер для переменной, объявление изменится, но код, как написано, работает ....
или ... поставьте маску или типизацию напрямую
В конце дня, если вы хотите, чтобы этот код работал, вы должны перенести его на новую платформу. Ваш выбор относительно того, как. Да, вы должны тратить время на каждый случай и делать это правильно, в противном случае вы будете продолжать возвращаться к этому коду, который еще дороже.
Если вы изолируете типы переменных в коде перед переносом и какой размер имеют типы переменных, то изолируйте переменные, которые это делают (должно быть легко найти), и измените их объявления, используя определения stdint.h, которые, надеюсь, не изменятся в будущем, и вы будете удивлены, но иногда используются неправильные заголовки, так что даже вставьте чеки, чтобы вы могли лучше спать по ночам
И хотя этот стиль кодирования работает (если (~ counter) counter ++;), для переносимости, желаемой сейчас и в будущем, лучше всего использовать маску, чтобы специально ограничивать размер (а не полагаться на объявление), делайте это, когда Код написан в первую очередь или просто закончите порт, а затем вам не придется переносить его снова в другой день. Или, чтобы сделать код более читабельным, тогда выполните if x <0xFF then или x! = 0xFF или что-то в этом роде, и компилятор может оптимизировать его под тот же код, что и для любого из этих решений, просто делая его более читабельным и менее рискованным. ...
Зависит от того, насколько важен продукт или сколько раз вы хотите отправлять исправления / обновления, кататься на грузовике или идти в лабораторию, чтобы решить, пытаетесь ли вы найти быстрое решение или просто коснитесь затронутых строк кода. если это всего лишь сто или несколько, это не так уж велик для порта.
источник
C 2011 Онлайн проект
Проблема заключается в том, что операнд
~
повышаетсяint
до применения оператора.К сожалению, я не думаю, что есть легкий выход из этого. Письмо
не поможет, потому что там действуют акции. Единственное, что я могу предложить, - это создать несколько символических констант для максимального значения, которое вы хотите, чтобы этот объект представлял, и проверить это:
источник
-1
это не нужно, так как это приведет к тому, что счетчик установится на 254 (0xFE). В любом случае, этот подход, как упоминалось в моем вопросе, не идеален из-за разных размеров переменных в кодовой базе, которые участвуют в этой идиоме.