Is ((a + (b & 255)) & 255) совпадает с ((a + b) & 255)?

Я просматривал код на С++ и нашел что-то вроде этого:

(a + (b & 255)) & 255

Двойной И раздражал меня, поэтому я подумал:

(a + b) & 255

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

Я быстро написал тест script (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

Они одинаковы. Здесь доказательство:

Сначала обратите внимание на тождество (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

завершая доказательство.

Ответ 2

При позиционном сложении, вычитании и умножении чисел без знака для получения результатов без знака более значимые цифры ввода не влияют на менее значимые цифры результата. Это относится как к двоичной арифметике, так и к десятичной арифметике. Это также относится к арифметике со знаком "дополнение к двум", но не к арифметике со знаком со знаком.

Однако мы должны быть осторожны, принимая правила из двоичной арифметики и применяя их к C (я верю, C++ имеет те же правила, что и C для этого материала, но я не уверен на 100%), потому что C арифметика имеет некоторые тайные правила, которые могут подвезти нас. Арифметика без знака в C следует простым двоичным правилам обертки, но переполнение арифметики со знаком - неопределенное поведение. Хуже того, в некоторых случаях C автоматически "продвигает" неподписанный тип в (подписанный) int.

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


Итак, возвращаясь к формуле в вопросе, эквивалентность зависит от типов операндов.

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

Если они являются целыми числами без знака, размер которых меньше, чем int, тогда они будут повышены до (со знаком) int. Переполнение целых чисел со знаком - неопределенное поведение, но, по крайней мере, на каждой платформе, с которой я столкнулся, разница в размере между разными целочисленными типами достаточно велика, чтобы одно добавление двух повышенных значений не вызывало переполнения. Итак, снова мы можем вернуться к просто двоичному арифметическому аргументу, чтобы считать операторы эквивалентными.

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

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

Ответ 3

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

Без знака 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), это поведение undefined. Таким образом, равенство не может быть выполнено - бывают случаи, когда (a+b)&255 является undefined, но (a+(b&255))&255 не является.

Ответ 4

Да, (a + b) & 255 отлично.

Помните о дополнении в школе? Вы добавляете цифры цифрами по цифре и добавляете значение переноса в следующий столбец цифр. Нет возможности для более позднего (более значительного) столбца цифр влиять на уже обработанный столбец. Из-за этого, это не имеет значения, если вы обнуляете цифры только в результате или также сначала в аргументе.


Вышеупомянутое не всегда верно, стандарт С++ допускает реализацию, которая нарушит это.

Такая станция смерти 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 promotion разрешен только в том случае, если int может представлять все значения типа источника (§4.5/1), поэтому int должен иметь не менее 32 бит, способствующих значению, плюс знаковый бит.
  • int не может иметь больше битов значения (не считая знакового бита), чем 32, потому что иначе добавление не может переполняться.

Ответ 5

У вас уже есть умный ответ: арифметика без знака является по модулю арифметикой, и поэтому результаты будут выполнены, вы можете доказать это математически...


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

И, в соответствии с Clang: выполняется равенство.

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

Примечание. Как компилятор перечисляет все 64-битные шаблоны в разумные сроки? Это не может. Петли были оптимизированы. В противном случае мы все умерли до прекращения выполнения.


Я изначально только доказал это для 16-разрядных целых чисел без знака; К сожалению, С++ - это сумасшедший язык, где маленькие целые числа (меньшие биты, чем 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;
}

И снова в соответствии с Clang: выполняется равенство.

Ну, вот и вы:)


1 Конечно, если программа когда-либо непреднамеренно вызывает Undefined Поведение, это не будет очень много.

Ответ 6

Быстрый ответ: оба выражения эквивалентны

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

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

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

  • И наоборот. Если тип a и b меньше, чем int, оба повышаются до int, а вычисление выполняется с использованием арифметики со знаком, где переполнение вызывает поведение undefined.

    • Если int имеет как минимум 33 бита значения, ни одно из вышеприведенных выражений не может переполняться, поэтому результат отлично определен и имеет одинаковое значение для обоих выражений.

    • Если int имеет ровно 32 бита значения, вычисление может переполнение для выражений, например, значения a=0xFFFFFFFF и b=1 переполнение в обоих выражениях. Чтобы этого избежать, вам нужно написать ((a & 255) + (b & 255)) & 255.

  • Хорошей новостью является отсутствие таких платформ 1.


1 Точнее, такой реальной платформы нет, но можно было бы настроить DS9K, чтобы проявить такое поведение и все еще соответствуют стандарту C.

Ответ 7

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

Ответ 8

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

При добавлении каждый бит влияет только на тех, кто более значителен, чем сам; никогда не менее значимые.

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

Ответ 9

Доказательство тривиально и осталось как упражнение для читателя

Но чтобы на самом деле узаконить это как ответ, ваша первая строка кода говорит, что последние 8 бит b ** (все старшие биты b установлены на ноль) и добавьте это в a, а затем возьмите только последние 8 бит результата, установив все высшие бит в ноль.

Во второй строке добавляются a и b и берут последние 8 бит со всеми более высокими битами 0.

В результате значительны только последние 8 бит. Поэтому на входе (входе) значительны только последние 8 бит.

** последние 8 бит = 8 LSB

Также интересно отметить, что результат будет эквивалентен

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

Как указано выше, только 8 LSB являются значительными, но результатом является unsigned int, при этом все остальные биты равны нулю. a + b будет переполняться, что приведет к ожидаемому результату.