Чи є ((a + (b & 255)) & 255 те саме, що ((a + b) & 255)?


92

Я переглядав якийсь код C ++ і знайшов щось подібне:

(a + (b & 255)) & 255

Подвійний І дратував мене, тому я подумав:

(a + b) & 255

( aі bє 32-розрядними цілими числами без знака)

Я швидко написав тестовий сценарій (JS), щоб підтвердити свою теорію:

for (var i = 0; i < 100; i++) {
    var a = Math.ceil(Math.random() * 0xFFFF),
        b = Math.ceil(Math.random() * 0xFFFF);

    var expr1 = (a + (b & 255)) & 255,
        expr2 = (a + b) & 255;

    if (expr1 != expr2) {
        console.log("Numbers " + a + " and " + b + " mismatch!");
        break;
    }
}

Хоча сценарій підтвердив мою гіпотезу (обидві операції рівні), я все ще не довіряю їй, тому що 1) випадкова і 2) я не математик, я не маю уявлення, що я роблю .

Крім того, вибачте за заголовок Lisp-y. Не соромтеся редагувати його.


4
Якою мовою це сценарій? Є чи Math.random()повертати ціле число або подвійні на [0,1)? Я не думаю, що ваш сценарій (найкраще, що я можу сказати) взагалі відображає проблему, яку ви поставили.
Цегла

7
Що таке код c / c ++? Вони різні мови.
Флюгер

14
Ви не можете відтворити поведінку, яку ви намагаєтеся перевірити в JS. Ось чому кожен - це лише ви щодо вибору мови. JS не набирається сильно, і відповідь критично залежить від типу змінних у C / C ++. JS - це повна нісенітниця, враховуючи питання, яке ви задали.
Цегла

4
@WeatherVane Це найважливіший псевдокод, використовуючи імена функцій Javascript. Його мова йде про поведінку &і +на беззнакових цілих чисел в C і C ++.
Бармар,

11
Майте на увазі, що "я написав тестову програму і отримав відповідь, яку очікував на всі можливі входи", насправді не є гарантією того, що щось поводиться так, як ви очікуєте. Невизначена поведінка може бути такою неприємною; даючи несподівані результати лише після того, як закінчите переконувати себе в правильності коду.

Відповіді:


78

Вони однакові. Ось доказ:

Спочатку зверніть увагу на особу (A + B) mod C = (A mod C + B mod C) mod C

Давайте повторимо проблему, вважаючи a & 255, що виступаєте за a % 256. Це правда, оскільки aне підписано.

Так само (a + (b & 255)) & 255є(a + (b % 256)) % 256

Це так само , як (a % 256 + b % 256 % 256) % 256(я застосував тотожність було сказано вище: зауважимо , що modі %. Чи еквівалентні для беззнакових типів)

Це спрощує те, (a % 256 + b % 256) % 256що стає (a + b) % 256(повторне застосування особистості). Потім можна повернути порозрядний оператор назад, щоб дати

(a + b) & 255

заповнення доказу.


81
Це математичний доказ, ігноруючи можливість переливу. Поміркуйте A=0xFFFFFFFF, B=1, C=3. Перша ідентичність не відповідає. (Переповнення не буде проблемою для непідписаної арифметики, але це трохи інша річ.)
AlexD

4
Власне, (a + (b & 255)) & 255це те саме (a + (b % 256)) % N % 256, що де Nзначення більше, ніж максимальне значення без знака. (остання формула повинна інтерпретуватися як арифметика математичних цілих чисел)

17
Такі математичні докази не підходять для доведення поведінки цілих чисел на комп'ютерних архітектурах.
Джек Ейдлі,

25
@JackAidley: Вони доречні, коли зроблено їх правильно (а це не так, через нехтування врахуванням переповнення).

3
@Shaz: Це стосується тестового сценарію, але не є частиною поставленого питання.

21

При позиційному додаванні, відніманні та множенні беззнакових чисел для отримання непідписаних результатів, більш значущі цифри введення не впливають на менш значущі цифри результату. Це стосується двійкової арифметики так само, як і десяткової арифметики. Це також стосується арифметики зі знаком "доповнення двох", але не арифметики зі знаком величини.

Однак ми повинні бути обережними, беручи правила з двійкової арифметики та застосовуючи їх до C (я вважаю, що C ++ має ті самі правила, що і C щодо цього матеріалу, але я не впевнений на 100%), оскільки арифметика C має деякі загадкові правила, які можуть нас спотикати вгору. Беззнакова арифметика на мові C відповідає простим двійковим правилам обгортання, але підписане арифметичне переповнення є невизначеною поведінкою. Гірше за певних обставин C автоматично "просуне" непідписаний тип до (підписаного) int.

Невизначена поведінка в C може бути особливо підступною. Тупий компілятор (або компілятор на низькому рівні оптимізації), швидше за все, зробить те, що ви очікуєте, базуючись на вашому розумінні двійкової арифметики, тоді як оптимізуючий компілятор може розбити ваш код дивними способами.


Отже, повертаючись до формули у питанні, еквівалентність залежить від типів операндів.

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

Якщо це цілі числа без знака, розмір яких менше, ніж intтоді, вони будуть підвищені до (підписані) int. Переповнення підписаних цілих чисел є невизначеною поведінкою, але принаймні на кожній платформі, з якою я стикався, різниця в розмірі між різними цілими типами є досить великою, щоб одне додавання двох висунутих значень не спричинило переповнення. Тож ми знову можемо повернутися до просто двійкового арифметичного аргументу, щоб вважати твердження еквівалентними.

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

OTOH якби aі bбули підписані цілими числами, розмір яких був більшим або рівним розміру int, тоді навіть у реалізаціях доповнення twos існують випадки, коли одне твердження було б чітко визначеним, а інше - невизначеною поведінкою.


20

Лема: a & 255 == a % 256для непідписаних a.

Unsigned aможна переписати в вигляді m * 0x100 + bдеяких без знака m, b, 0 <= b < 0xff, 0 <= m <= 0xffffff. З обох визначень випливає, щоa & 255 == b == a % 256 .

Додатково нам потрібні:

  • розподільне майно: (a + b) mod n = [(a mod n) + (b mod n)] mod n
  • визначення беззнакового додавання, математично: (a + b) ==> (a + b) % (2 ^ 32)

Отже:

(a + (b & 255)) & 255 = ((a + (b & 255)) % (2^32)) & 255      // def'n of addition
                      = ((a + (b % 256)) % (2^32)) % 256      // lemma
                      = (a + (b % 256)) % 256                 // because 256 divides (2^32)
                      = ((a % 256) + (b % 256 % 256)) % 256   // Distributive
                      = ((a % 256) + (b % 256)) % 256         // a mod n mod n = a mod n
                      = (a + b) % 256                         // Distributive again
                      = (a + b) & 255                         // lemma

Так що так, це правда. Для 32-розрядних цілих чисел без знака.


А як щодо інших цілочисельних типів?

  • Для 64-розрядних цілих чисел без знака, все вищесказане відноситься так само добре, як раз з заміною 2^64на 2^32.
  • Для 8- та 16-розрядних цілих чисел без знака додавання передбачає підвищення до int. Це intточно не буде ні переповнюватися, ні негативним у будь-якій з цих операцій, тому всі вони залишаються дійсними.
  • Для підписаних цілих чисел, якщо вони є a+bабо a+(b&255)переповнені, це невизначена поведінка. Тож рівність не може триматися - бувають випадки, коли (a+b)&255невизначена поведінка (a+(b&255))&255є, але ні.

17

Так, (a + b) & 255це нормально.

Пам'ятаєте доповнення в школі? Ви додаєте цифри цифрами за цифрами та додаєте значення перенесення до наступного стовпця цифр. Немає можливості для пізнішого (більш значущого) стовпця цифр вплинути на вже оброблений стовпець. Через це не має значення, якщо ви обнуляєте цифри лише в результаті або також спочатку в аргументі.


Вищезазначене не завжди відповідає дійсності, стандарт С ++ дозволяє реалізацію, яка це порушить.

Такий Deathstation 9000 : - ) повинен був би використовувати 33-біт int, якщо OP мав на увазі unsigned short"32-бітові цілі числа без знака". Якби unsigned intце малося на увазі, DS9K повинен був би використовувати 32-розрядну intта 32-розрядну unsigned intз бітом заповнення. (Цілі числа без підпису повинні мати той самий розмір, що і їх підписані аналоги згідно з §3.9.1 / 3, а біти заповнення допускаються в §3.9.1 / 1.) Інші комбінації розмірів та біти заповнення також працюють.

Наскільки я можу зрозуміти, це єдиний спосіб його зламати, оскільки:

  • Цілочисельне представлення повинно використовувати "суто двійкову" схему кодування (§3.9.1 / 7 та виноска), усі біти, крім бітів доповнення та знакового біта, повинні мати значення 2 n
  • просування int дозволяється лише у тому випадку, якщо intможе представляти всі значення типу джерела (§4.5 / 1), тому intмає бути принаймні 32 біта, що вносять значення, плюс знаковий біт.
  • intне може мати більше значення бітів (не рахуючи біт знака) , ніж 32, так як інакше додаток не може переповнення.

2
Існує багато інших операцій, крім додавання, де сміття у старших бітах не впливає на результат у низьких бітах, які вас цікавлять. Дивіться це запитання щодо додатку 2 , який використовує x86 asm як варіант використання, але також застосовується до беззнакові двійкові цілі числа в будь-якій ситуації.
Пітер Кордес,

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

2
Це, безумовно, найпростіша відповідь / аргумент для розуміння, IMO. Перенесення / запозичення додатково / віднімання поширюється лише від низьких бітів до високих бітів (справа наліво) у двійковій системі, так само, як у десятковій. IDK, чому хтось буде проти цього.
Пітер Кордес,

1
@Bathsheba: CHAR_BIT не повинен бути 8. Але непідписані типи в C та C ++ повинні поводитися як звичайні двійкові цілі числа base2 деякої бітової ширини. Я думаю, що для цього потрібно мати UINT_MAX 2^N-1. (Я, можливо, навіть не вимагає кратності CHAR_BIT, я забуваю, але я майже впевнений, що стандарт вимагає, щоб обгортання відбувалося за модулем деякої потужності 2.) Я думаю, що єдиний спосіб отримати дивацтва - це підвищення до підписаний тип, який є достатньо широким для зберігання aабо, bале недостатньо широким, щоб вмістити його a+bу всіх випадках.
Пітер Кордес,

2
@Bathsheba: так, на щастя, мова C-as-portable-Assembly-мова дійсно в основному працює для непідписаних типів. Навіть навмисно ворожа реалізація C не може цього зламати. Це лише підписані типи, де все справді жахливо для справді портативних біт-хаків на C, і Deathstation 9000 дійсно може зламати ваш код.
Пітер Кордес,

14

У вас вже є розумна відповідь: непідписана арифметика - це модульна арифметика, і, отже, результати будуть тримати, ви можете це математично довести ...


Однак одна крута річ щодо комп’ютерів полягає в тому, що комп’ютери працюють швидко. Дійсно, вони настільки швидкі, що перерахування всіх дійсних комбінацій із 32 бітів можливо за розумний проміжок часу (не намагайтеся використовувати 64 біти).

Отже, у вашому випадку мені особисто подобається просто кидати його в комп’ютер; мені потрібно менше часу, щоб переконати себе, що програма правильна, ніж потрібно, щоб переконати себе, ніж математичний доказ правильний і що я не контролював деталі в специфікації 1 :

#include <iostream>
#include <limits>

int main() {
    std::uint64_t const MAX = std::uint64_t(1) << 32;
    for (std::uint64_t i = 0; i < MAX; ++i) {
        for (std::uint64_t j = 0; j < MAX; ++j) {
            std::uint32_t const a = static_cast<std::uint32_t>(i);
            std::uint32_t const b = static_cast<std::uint32_t>(j);

            auto const champion = (a + (b & 255)) & 255;
            auto const challenger = (a + b) & 255;

            if (champion == challenger) { continue; }

            std::cout << "a: " << a << ", b: " << b << ", champion: " << champion << ", challenger: " << challenger << "\n";
            return 1;
        }
    }

    std::cout << "Equality holds\n";
    return 0;
}

Це перераховує всі можливі значення aі bв 32-біт простору і перевіряє , чи правильно рівність, чи ні. Якщо це не так, друкується корпус, який не працював, що можна використовувати як перевірку стану здоров’я.

І, думку Кланга : Рівність має місце .

Крім того, враховуючи, що арифметичні правила є агностичними по ширині бітів (вище intбітової ширини), ця рівність буде виконуватися для будь-якого цілого беззнакового типу, що становить 32 біти або більше, включаючи 64 біти і 128 бітів.

Примітка: Як компілятор може перерахувати всі 64-бітові шаблони в розумні часові рамки? Це не може. Петлі були оптимізовані. Інакше ми всі загинули б до припинення страти.


Спочатку я довів це лише для 16-бітових цілих чисел без знака; на жаль, C ++ - це божевільна мова, де intспочатку перетворюються малі цілі числа (менші бітові ширини ніж )int .

#include <iostream>

int main() {
    unsigned const MAX = 65536;
    for (unsigned i = 0; i < MAX; ++i) {
        for (unsigned j = 0; j < MAX; ++j) {
            std::uint16_t const a = static_cast<std::uint16_t>(i);
            std::uint16_t const b = static_cast<std::uint16_t>(j);

            auto const champion = (a + (b & 255)) & 255;
            auto const challenger = (a + b) & 255;

            if (champion == challenger) { continue; }

            std::cout << "a: " << a << ", b: " << b << ", champion: "
                      << champion << ", challenger: " << challenger << "\n";
            return 1;
        }
    }

    std::cout << "Equality holds\n";
    return 0;
}

І ще раз, за словами Кланга : рівність має місце .

Ну ось і ви :)


1 Звичайно, якщо програма коли-небудь випадково викликає невизначену поведінку, це не доведе багато.


1
ти кажеш, це легко зробити з 32-бітовими значеннями, але насправді використовувати 16-бітові ...: D
Віллі Менцель

1
@WilliMentzel: Це цікаве зауваження. Спочатку я хотів сказати, що якщо він працює з 16 бітами, то він буде працювати так само з 32 бітами, 64 бітами та 128 бітами, оскільки Стандарт не має специфічної поведінки для різних ширин бітів ... однак я згадав, що насправді він працює для ширини бітів, меншої, ніж int:: малі цілі числа спочатку перетворюються в int(дивне правило). Тож я фактично маю провести демонстрацію з 32-бітами (а згодом вона поширюється на 64 біти, 128 бітів, ...).
Матьє М.,

2
Оскільки ви не можете оцінити всі (4294967296 - 1) * (4294967296 - 1) можливі результати, ви якось зменшуєте? Я вважаю, MAX повинен бути (4294967296 - 1), якщо ви підете цим шляхом, але він ніколи не закінчиться протягом нашого життя, як ви сказали ... так, зрештою, ми не можемо показати рівність в експерименті, принаймні не в такому, як ви опишіть.
Віллі Менцель,

1
Тестування цього на реалізації комплементу One 2 не доводить, що його можна переносити зі знаком величини або своїм комплементом із ширинами типу Deathstation 9000. наприклад, вузький непідписаний тип може перейти до 17-бітового, intякий може представляти всі можливі uint16_t, але де a+bможе переповнюватися. Це проблема лише для беззнакових типів, вужчих ніж int; C вимагає, щоб unsignedтипи були двійковими цілими числами, тому обгортання відбувається за модулем степеня 2
Пітер Кордес,

1
Погодились з тим, що C занадто портативний для власного блага. Було б дуже добре, якби вони стандартизували доповнення 2, арифметичні зрушення вправо для підписаних та спосіб зробити підписану арифметику із семантикою обгортання замість семантики невизначеної поведінки для тих випадків, коли потрібно обгортання. Тоді C знову може бути корисним як портативний асемблер, замість мінного поля завдяки сучасним оптимізаторським компіляторам, які роблять небезпечним залишити будь-яку невизначену поведінку (принаймні для вашої цільової платформи. Невизначена поведінка лише на реалізаціях Deathstation 9000 - це нормально, як ви вказати).
Пітер Кордес,

4

Швидка відповідь: обидва вирази еквівалентні

  • оскільки aі bє 32-розрядними цілими числами без знака, результат однаковий навіть у випадку переповнення. беззнакова арифметика гарантує це: результат, який не може бути представлений результуючим цілим беззнаковим типом, зменшується за модулем на число, яке на одне перевищує найбільше значення, яке може бути представлене отриманим типом.

Довга відповідь така: немає відомих платформ, де ці вирази відрізнялися б, але Стандарт не гарантує цього через правила інтегрального просування.

  • Якщо тип aі b(беззнакові 32-бітові цілі числа) має вищий ранг, ніж int, обчислення виконується як беззнакове, за модулем 2 32 , і воно дає однаковий визначений результат для обох виразів для всіх значень aі b.

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

    • Якщо intмає принаймні 33 біти значення, жоден із наведених вище виразів не може переповнюватися, тому результат ідеально визначений і має однакове значення для обох виразів.

    • Якщо intмає рівно 32 біти значення, обчислення може переповнюватися для обох виразів, наприклад значень, a=0xFFFFFFFFі b=1призведе до переповнення обох виразів. Щоб цього уникнути, вам потрібно буде написати ((a & 255) + (b & 255)) & 255.

  • Хороша новина полягає в тому, що таких платформ немає 1 .


1 Точніше, такої реальної платформи не існує, але можна налаштувати DS9K, щоб він демонстрував таку поведінку і при цьому відповідав стандарту C.


3
Ваша друга підбула вимагає (1) aменше, ніж int(2), intмає 32 біти значення (3) a=0xFFFFFFFF. Це не може бути правдою.
Barry

1
@Barry: Один випадок, який, здається, відповідає вимогам, - це 33-біт int, де є 32 біти значення та один біт знаку.
Бен Войгт

2

Ідентично припускаючи відсутність переповнення . Жодна з версій по-справжньому не захищена від переповнення, але подвійна версія є стійкішою до неї. Мені не відома система, коли переповнення в цьому випадку є проблемою, але я бачу, як автор робить це, якщо така є.


1
Вказаний OP: (a і b - це 32-розрядні цілі числа без знака) . Якщо intширина не становить 33 біти, результат однаковий навіть у випадку переповнення. беззнакова арифметика гарантує це: результат, який не може бути представлений результуючим цілим беззнаковим типом, зменшується за модулем на число, яке на одне перевищує найбільше значення, яке може бути представлене отриманим типом.
chqrlie

2

Так, ви можете це довести арифметикою, але є більш інтуїтивна відповідь.

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

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


0

Доказ є тривіальним і залишається вправою для читача

Але, щоб насправді узаконити це як відповідь, ваш перший рядок коду говорить, що візьміть останні 8 бітів b ** (усі старші біти bвстановлені в нуль), додати це до, aа потім взяти лише останні 8 бітів результату, встановивши все вище біти до нуля.

У другому рядку написано add a і bприймання останніх 8 бітів з усіма старшими бітами нулем.

Лише останні 8 біт є значущими в результаті. Тому лише останні 8 бітів є значущими на вході.

** останні 8 бітів = 8 LSB

Також цікаво відзначити, що вихідний результат буде еквівалентним

char a = something;
char b = something;
return (unsigned int)(a + b);

Як і вище, значущими є лише 8 LSB, але результат - unsigned intз усіма іншими бітами нуль. a + bПереповниться, виробляючи очікуваний результат.


Ні, не було б. Char математика відбувається, оскільки int та char можуть бути підписані.
Антті
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.