Послідовна операція призводить до несподіваних змінних розмірів


24

Контекст

Ми переносимо код C, який спочатку був складений за допомогою 8-бітного компілятора С для мікроконтролера 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т. Д.), І їх зміна в інакше працюючій кодовій базі не те, що ми з нетерпінням чекаємо.

Питання

Чи правильне наше розуміння проблеми, і чи є варіанти, щоб вирішити цю проблему, які не потребують повторного відвідування кожного випадку, коли ми використовували цю ідіому? Чи правильне наше припущення, що операція на зразок бітового доповнення повинна повертати результат, який має той самий розмір, що й операнд? Схоже, це зламається, залежно від архітектури процесора. Я відчуваю, що я приймаю божевільні таблетки і що С повинен бути трохи більш портативним, ніж цей. Знову ж, наше розуміння цього може бути неправильним.

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


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

Я не бачив фактичної суті проблеми, що обговорювалася там, а саме, розмір результату побітового доповнення відрізняється від того, що передається оператору.


14
"Чи правильне наше припущення, що операція на зразок бітового доповнення повинна повертати результат, який має той самий розмір, що й операнд?" Ні, це неправильно, застосовуються цілі акції.
Томас Джагер

2
Незважаючи на те, що це актуально, я не переконаний, що це дублікати цього конкретного питання, оскільки вони не дають рішення проблеми.
Коді Грей

3
Я відчуваю, що я приймаю божевільні таблетки і що С повинен бути трохи більш портативним, ніж цей. Якщо ви не отримали цілі акції на 8-бітних типах, ваш компілятор не був сумісним зі стандартом C. У цьому випадку я думаю, що вам слід пройти всі обчислення, щоб перевірити їх та, якщо потрібно, виправити.
користувач694733

1
Мене єдине цікавить, яка логіка, окрім дійсно неважливих лічильників, може прийняти її до «приросту, якщо місця достатньо, ще забудьте»? Якщо ви переносите код, чи можете ви використовувати int (4 байти) замість uint_8? Це заважало б вашій проблемі в багатьох випадках.
шайба

1
@puck Ви праві, ми могли змінити його на 4 байти, але це порушило б сумісність під час спілкування з існуючими системами. Наміром є знати, коли є якісь помилки, і тому 1-байтовий лічильник спочатку був достатнім, і залишається таким.
Чарлі Солт

Відповіді:


26

Те, що ви бачите, є результатом цілих акцій . У більшості випадків, коли в виразі використовується ціле значення, якщо тип значення менший, ніж intсприяє значенню int. Це задокументовано у розділі 6.3.1.1p2 стандарту С :

Наступне може бути використаний в вираженні всюди , де intабо unsigned intможе бути використаний

  • Об'єкт або вираз із цілим типом (відмінним від intабо unsigned int), чиє число цілочислових перетворень менше або дорівнює рангу intі unsigned int.
  • Бітове поле типу _BoolБітове , int ,підписане int , orunsigned int`.

Якщо a intможе представляти всі значення вихідного типу (як обмежені шириною, для бітового поля), значення перетворюється в an 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 дає найбільше позитивне значення.


1
if (!++counter) --counter;може бути менш дивним для деяких програмістів, ніж використання оператора кома.
Ерік Postpischil

1
Ще одна альтернатива ++counter; counter -= !counter;.
Eric Postpischil

@EricPostpischil Насправді мені більше подобається твій перший варіант. Відредаговано.
dbush

15
Це некрасиво і нечитабельно, незалежно від того, як ви це пишете. Якщо вам доводиться використовувати подібну фразу, зробіть кожному програмісту технічного обслуговування послугу і перетворіть її як вбудовану функцію : щось на зразок increment_unsigned_without_wraparoundабо increment_with_saturation. Особисто я використовував би загальну триоперандну clampфункцію.
Коді Грей

5
Крім того, ви не можете зробити цю функцію, оскільки вона повинна поводитися по-різному для різних типів аргументів. Вам доведеться використовувати загальний тип макросу .
user2357112 підтримує Моніку

7

за розміромof (i); Ви вимагаєте розміру змінної i , так що 1

у розміріof (~ i); ви запитуєте розмір типу виразу, який є цілим , у вашому випадку 4


Використовувати

if (~ i)

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

if (i != 255)

і у вас з'явиться портативний і читабельний код


Існує кілька розмірів змінних (наприклад, uint16_t та неподписаний знак тощо)

Щоб керувати будь-яким розміром без підпису:

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

Вираз постійний, тому обчислюється під час компіляції.

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


3
Питання прямо говорить, що вони мають кілька розмірів для вирішення, тому != 255недостатньо.
Eric Postpischil

@EricPostpischil, так, я це забуваю, так що "якщо (i! = ((1u << sizeof (i) * 8) - 1))" припускаючи, що завжди не підписано?
бруно

1
Це не буде визначено для unsignedоб'єктів, оскільки зрушення повної ширини об'єкта не визначені стандартом C, але це можна виправити за допомогою (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil

о так офіційно, CHAR_BIT, моє погано
бруно

2
Для безпеки з більш широкими типами можна використовувати ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil

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 має вбудовані функції. Ви нічого не робите всередині цього визначення макросу, що неможливо легко виконати всередині вбудованої функції. І якщо ви збираєтесь використовувати макрос, переконайтеся, що ви стратегічно скористаєтеся дужками для чіткості: оператор << має дуже низький пріоритет. Кланг попереджає про це с -Wshift-op-parentheses. Хороша новина полягає в тому, що оптимізуючий компілятор не збирається генерувати поділ тут, тож вам не доведеться турбуватися про те, що це повільно.
Коді Грей

1
@CodyGray, якщо ви думаєте, що це можна зробити за допомогою функції, напишіть відповідь.
Карстен S

2
@CodyGray: sizeof xнеможливо реалізувати всередині функції C, оскільки xвін повинен бути параметром (або іншим виразом) з певним фіксованим типом. Він не може створити розмір будь-якого типу аргументу, який використовує абонент. Можна макрос
Ерік Postpischil

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);

І хоча цей стиль кодування працює (якщо (~ лічильник) лічильник ++;), для бажання переносимості зараз і в майбутньому найкраще використовувати маску, щоб спеціально обмежити розмір (а не покладатися на декларацію), робіть це, коли Код пишеться в першу чергу або просто закінчити порт, і тоді вам не доведеться повторно перенести його ще якийсь день. Або щоб зробити код більш читабельним, тоді зробіть, якщо x <0xFF тоді або 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). У будь-якому випадку, такий підхід, як згадувалося в моєму питанні, не є ідеальним через різні змінні розміри в кодовій базі, які беруть участь у цій ідіомі.
Чарлі Сольс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.