((a +(b&255))&255)は((a + b)&255)と同じですか?


92

私はいくつかのC ++コードを閲覧していて、次のようなものを見つけました:

(a + (b & 255)) & 255

ダブルANDに悩まされたので、次のように考えました。

(a + b) & 255

aおよびb32ビットの符号なし整数です)

私はすぐに私の理論を確認するためのテストスクリプト(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 ++コードとは何ですか?彼らは異なる言語です。
Weather Vane

14
JSでテストしようとしている動作を再現することはできません。だからこそ、誰もが言語の選択についてあなただけです。JSは強く型付けされておらず、答えはC / C ++の変数の型に大きく依存します。あなたが尋ねた質問を考えると、JSは完全にナンセンスです。
ブリック

4
@WeatherVaneこれは、JavaScript関数名を使用した本質的な疑似コードです。彼の質問はの行動についてです&+CおよびC ++での符号なし整数に。
Barmar 2016年

11
「テストプログラムを作成して、すべての可能な入力に対して期待した答えを得た」というのは、実際に期待どおりの動作をすることを保証するものではないことに注意してください。未定義の動作はそのような厄介なものになる可能性があります。コードが正しいことを納得させた後でのみ、予期しない結果を出します。

回答:


78

彼らは同じです。ここに証明があります:

最初にアイデンティティに注意してください (A + B) mod C = (A mod C + B mod C) mod C

の代役と見なして、問題を再度説明a & 255a % 256ます。aは符号なしなので、これは真実です。

そう(a + (b & 255)) & 255です(a + (b % 256)) % 256

これは同じです(a % 256 + b % 256 % 256) % 256(上記のIDを適用しました。これは、署名されていない型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符号なしの最大値よりも1つ大きくなります。(後者の式は、数学的な整数の算術として解釈されることを意図しています)

17
このような数学的証明は、コンピュータアーキテクチャでの整数の動作を証明するのには適していません。
Jack Aidley、2016年

25
@JackAidley:正しく行われた場合に適切です(オーバーフローの考慮を怠っているため、正しくありません)。

3
@Shaz:これはテストスクリプトにも当てはまりますが、質問の一部ではありません。

21

符号なしの数値を位置加算、減算、乗算して符号なしの結果を生成する場合、入力の上位桁が結果の下位桁に影響を与えることはありません。これは、10進数演算と同じように2進数演算に適用されます。また、「2の補数」の符号付き算術にも適用されますが、符号の大きさの符号付き算術には適用されません。

ただし、バイナリ算術からルールを取得してCに適用するときは注意する必要があります(C ++にはCと同じルールがあると思いますが、100%確実ではありません)。アップ。Cの符号なし算術は単純なバイナリラップアラウンドルールに従いますが、符号付き算術オーバーフローは未定義の動作です。さらに悪いことには、Cは符号なしの型を(符号付き)intに自動的に「昇格」させます。

Cでの未定義の動作は、特に油断ならないことがあります。ダムコンパイラ(または最適化レベルが低いコンパイラ)は、バイナリ演算の理解に基づいて期待どおりの動作をする可能性がありますが、最適化コンパイラは奇妙な方法でコードを壊す可能性があります。


したがって、問題の公式に戻ると、同等性はオペランドのタイプによって異なります。

サイズがのサイズ以上の符号なし整数の場合int、加算演算子のオーバーフロー動作は単純なバイナリラップアラウンドとして明確に定義されています。加算演算の前に1つのオペランドの上位24ビットをマスクするかどうかは、結果の下位ビットには影響しません。

サイズがそれよりも小さい符号なし整数の場合、それらはint(符号付き)に昇格されintます。符号付き整数のオーバーフローは未定義の動作ですが、少なくとも私が遭遇したすべてのプラットフォームで、異なる整数型間のサイズの違いは大きすぎて、2つの昇格された値を1回加算してもオーバーフローは発生しません。したがって、ここでも、単純な2項算術引数にフォールバックして、ステートメントを同等と見なすことができます。

それらがintより小さいサイズの符号付き整数である場合、再びオーバーフローは発生せず、2の補数の実装では、標準のバイナリ算術引数を使用してそれらが同等であると言うことができます。符号の大きさや実装を補完するものでは、それらは同等ではありません。

OTOH サイズがintのサイズ以上の符号付き整数の場合ab2の補数の実装でも、1つのステートメントが適切に定義されていても、他のステートメントが未定義の動作になる場合があります。


20

補題:a & 255 == a % 256署名なしa

符号なしのaように書き換えることができるm * 0x100 + bいくつかの符号なしmb0 <= b < 0xff0 <= 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大丈夫です。

学校での追加を覚えていますか?数字を数字ごとに追加し、桁上げ値を数字の次の列に追加します。後の(より重要な)桁の列が既に処理された列に影響を与える方法はありません。このため、結果の桁のみ、または引数の最初の桁をゼロにしても、違いはありません。


上記は常に当てはまるとは限りません。C++標準では、これを破る実装が許可されています。

このようなDeathstation 9000 - 33ビットを使用する必要がありますintOPは意味場合は、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
追加以外にも、上位ビットのガベージが関心のある下位ビットの結果に影響を与えない他の多くの操作があります。x86asmをユースケースとして使用する2の補数に関するこのQ&Aを参照してください。あらゆる状況での符号なし2進整数。
Peter Cordes

2
もちろん、誰もが匿名で反対投票する権利はありますが、学ぶ機会としてコメントを常に感謝しています。
アラン

2
これは、理解するのにはるかに簡単な答え/議論です、IMO。加算/減算のキャリー/ボローは、10進数の場合と同様に、2進数で下位ビットから上位ビット(右から左)にのみ伝播します。IDKなぜ誰かがこれに反対票を投じるのか。
Peter Cordes

1
@Bathsheba:CHAR_BITは8である必要はありませんが、CおよびC ++の符号なしの型は、いくつかのビット幅の通常のbase2バイナリ整数として動作する必要があります。UINT_MAXが必要です2^N-1。(NはCHAR_BITの倍数である必要さえないかもしれませんが、忘れますが、標準ではラップアラウンドが2のべき乗で行われる必要があると確信しています。)奇妙さを得る唯一の方法は、保持するのに十分な幅があるaか、すべてのケースでb保持するのa+bに十分な幅ではない符号付きタイプ。
Peter Cordes

2
@Bathsheba:はい、幸いなことに、C-as-portable-assembly-languageは、実際にはほとんど符号なしの型で機能します。故意に悪意のあるCの実装でさえ、これを破ることはできません。これは、Cでの真に移植可能なビットハックがおそろしいものであり、Deathstation 9000が実際にコードを破ることができる、署名された型のみです。
Peter Cordes

14

あなたはすでに賢い答えを持っています:符号なし算術はモジュロ算術であり、したがって結果が保持され、数学的にそれを証明することができます...


ただし、コンピュータの優れた点の1つは、コンピュータが高速であることです。実際、それらは非常に高速であり、妥当な時間内に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;
}

これを列挙の全ての可能な値を通るab平等が成立する、またはしないかどうかを32ビットの空間をチェックします。そうでない場合は、機能しなかったケースを出力します。これは、サニティチェックとして使用できます。

そして、クランによると平等を保持しています

さらに、算術規則がビット幅にとらわれない(intビット幅を超える)場合、この等式は、64ビットと128ビットを含む32ビット以上の符号なし整数型にも適用されます。

注:コンパイラーは、すべての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;
}

そしてもう一度、Clangよると平等が成立します

さて、あなたは行き​​ます:)


1 もちろん、プログラムが不注意で未定義の動作を引き起こしたとしても、それはあまり証明されません。


1
32ビットの値を使用するのは簡単ですが、実際には16ビットの値を使用します...:D
Willi Mentzel

1
@WilliMentzel:それは興味深い発言です。私は最初にそれが16ビットで動作する場合、標準は異なるビット幅に対して特定の動作を持たないため、32ビット、64ビット、および128ビットでも同じように動作することを言いたかったのですが...のビット幅より小さい場合int:最初に小さな整数がint(奇妙な規則)に変換されます。したがって、実際には32ビットでデモを行う必要があります(その後、64ビット、128ビットに拡張されます)。
Matthieu M.

2
(4294967296-1)*(4294967296-1)の可能性のあるすべての結果を評価することはできないので、どうにかして削減しますか?私の意見では、MAXは(4294967296-1)になるはずです。ただし、あなたが言ったように、私たちの生涯内に終了することはありません...説明する。
Willi Mentzel 2016年

1
1の2の補数の実装でこれをテストしても、符号の大きさや、Deathstation 9000タイプの幅で補数に移植できることは証明されません。たとえば、幅の狭い符号なしタイプは、intあらゆる可能性を表すことができる17ビットに昇格できますuint16_tが、a+bオーバーフローする可能性があります。これは、より狭い符号なし型の場合にのみ問題になりintます。Cはunsigned型がバイナリ整数であることを要求するので、ラップアラウンドは2の累乗を法として起こります
Peter Cordes

1
Cは自分の利益のために移植性が高すぎることに同意しました。それは次のようになり、本当にあなたがするとき、彼らはそれらのケースのための2の補数、署名のための算術右シフトし、代わりに未定義の行動の意味のラッピングセマンティクスで署名した算術演算を実行する方法、を標準化したい場合には素敵たいラッピングを。次に、最新の最適化コンパイラのおかげで地雷原の代わりにCがポータブルアセンブラとして再び役立つ可能性があり、未定義の動作を残しておくことは安全ではありません(少なくともターゲットプラットフォームに対して)。指摘する)。
Peter Cordes 2016年

4

簡単な答えは次のとおりです。両方の式は同等です

  • 以降ab32ビットの符号なし整数であり、結果はさらに、オーバーフローの場合も同様です。符号なし算術はこれを保証します。結果の符号なし整数型で表現できない結果は、結果の型で表現できる最大値より1大きい数を法として減じられます。

長い答えは次のとおりです。これらの表現が異なる既知のプラットフォームはありませんが、統合的なプロモーションのルールのため、標準はそれを保証しません。

  • 種類場合ab(符号なし32ビット整数)よりも高いランクを有するint、計算は、符号なしとして行わ2モジュロさ32を、そしてそれはすべての値について、両方式の同一の定義された結果が得られるaとしb

  • 逆に、入力した場合ab比べて小さくなっているintに昇格さの両方、intおよび計算がオーバーフロー呼び出すは動作を未定義場合、算術符号付きを用いて行われます。

    • intに少なくとも33個の値ビットがある場合、上記の式はどちらもオーバーフローすることがないため、結果は完全に定義され、両方の式で同じ値になります。

    • 場合int、正確に32の値のビットを有し、計算ができのオーバーフローの両方例えば値について、式a=0xFFFFFFFFb=1の両方式でオーバーフローを引き起こします。これを回避するには、を記述する必要があります((a & 255) + (b & 255)) & 255

  • 良いニュースは、そのようなプラットフォームがないことです1


1より正確には、そのような実際のプラットフォームは存在しませんが、DS9Kを構成してそのような動作を示し、C標準に準拠することができます。


3
2番目のサブブレットには、(1)aint(2)int32の値ビットよりも小さい(3)が必要a=0xFFFFFFFFです。それらすべてが本当であるとは限らない。
2016年

1
@Barry:要件を満たしていると思われる1つのケースは33ビットでint、32の値ビットと1つの符号ビットがあります。
Ben Voigt 2018年

2

オーバーフローがないことを前提しています。どちらのバージョンも、オーバーフローの影響を完全に免れることはできませんが、ダブルとバージョンは、オーバーフローに対してより耐性があります。私は、この場合のオーバーフローが問題になるシステムを認識していませんが、存在する場合に著者がこれを行っているのを見ることができます。


1
指定されたOP:(aおよびbは32ビットの符号なし整数です)int33ビット幅でない限り、オーバーフローが発生しても結果は同じです。符号なし算術はこれを保証します。結果の符号なし整数型で表現できない結果は、結果の型で表現できる最大値より1大きい数を法として減じられます。
chqrlie 2016年

2

はい、算数で証明できますが、より直感的な答えがあります。

追加するとき、すべてのビットはそれ自体よりも重要なものにのみ影響します。重要度の低いものは決してありません。

したがって、ビットを変更した最下位ビットよりも重要度を低くしない限り、加算の前に上位ビットに対して行った結果は変化しません。


0

証明は取るに足らないものであり、読者のための練習問題として残されています

しかし、これを答えとして実際に正当化するために、最初のコード行はb**の最後の8ビット(すべての上位ビットbをゼロに設定)をa取り、これに追加して、結果設定の最後の8ビットのみをすべて上位に取得することを示していますビットをゼロに。

2行目は、追加ab、最後の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に署名できるため発生します。
Antti Haapala
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.