私はいくつかのC++コードを閲覧していて、次のようなものを見つけました。
(a + (b & 255)) & 255
ダブルANDは私を悩ませたので、私は考えました:
(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タイトルについても申し訳ありません。自由に編集してください。
それらは同じです。ここに証拠があります:
最初にアイデンティティ(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
と同じです(上記のIDを適用しました:mod
と%
は符号なしの型では同等です。)
これは(a % 256 + b % 256) % 256
に簡略化され、(a + b) % 256
(IDの再適用)になります。次に、ビットごとの演算子を元に戻して、
(a + b) & 255
証明を完了します。
符号なしの結果を生成するための符号なしの数値の位置の加算、減算、および乗算では、入力の有効桁数が結果の下位桁に影響しません。これは、10進数演算と同様に、2進演算にも適用されます。また、「2の補数」の符号付き算術にも適用されますが、符号の大きさの符号付き算術には適用されません。
しかし、バイナリ演算からルールを取得してCに適用するときは注意する必要があります(C++にはこのようなものと同じルールがありますが、100%確信はありません)アップ。 Cの符号なし算術は、単純なバイナリラップアラウンドルールに従いますが、符号付き算術オーバーフローは未定義の動作です。状況によってはさらに悪いことに、Cは自動的にunsigned型を(signed)intに「昇格」させます。
Cでの未定義の動作は、特に油断できません。ダムコンパイラ(または低最適化レベルのコンパイラ)は、バイナリ算術の理解に基づいて期待どおりの動作をする可能性がありますが、最適化コンパイラは奇妙な方法でコードを破壊する可能性があります。
したがって、問題の式に戻ると、同等性はオペランドのタイプに依存します。
サイズがint
のサイズ以上の符号なし整数である場合、加算演算子のオーバーフロー動作は単純なバイナリラップアラウンドとして明確に定義されています。加算演算の前に1つのオペランドの上位24ビットをマスクするかどうかは、結果の下位ビットに影響しません。
サイズがint
より小さい符号なし整数の場合、それらは(符号付き)int
に昇格されます。符号付き整数のオーバーフローは未定義の動作ですが、少なくとも、異なる整数型間のサイズの違いに遭遇したすべてのプラットフォームでは、2つの昇格された値を1回追加してもオーバーフローは発生しません。したがって、単純なバイナリ算術引数にフォールバックして、ステートメントを同等とみなすことができます。
サイズがintより小さい符号付き整数の場合、再びオーバーフローは発生せず、2の補数の実装では、標準のバイナリ算術引数に依存して同等であると判断できます。符号の大きさや実装を補完するものでは、同等ではありません。
OTOH a
とb
がintのサイズ以上のサイズの符号付き整数である場合、2の補数実装でも、一方のステートメントが適切に定義されている場合があります。未定義の動作になります。
補題:符号なしa
の場合はa & 255 == a % 256
。
符号なし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)
副<文>この[前述の事実の]結果として、それ故に、従って、だから◆【同】consequently; therefore <文>このような方法で、このようにして、こんなふうに、上に述べたように◆【同】in this manner <文>そのような程度まで<文> AひいてはB◆【用法】A and thus B <文>例えば◆【同】for example; as an example:
(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ビットの符号なし整数の場合。
他の整数型はどうですか?
2^64
を2^32
に置き換えます。int
への昇格が含まれます。このint
は、これらの操作のいずれにおいても確実にオーバーフローしたり負になったりしないため、すべて有効のままです。a+b
またはa+(b&255)
オーバーフローの場合、未定義の動作です。そのため、平等は成り立ちません— (a+b)&255
は未定義の動作ですが、(a+(b&255))&255
はそうではない場合があります。はい、(a + b) & 255
で問題ありません。
学校での追加を覚えていますか?数字を1桁ずつ追加し、桁の次の列に桁上げ値を追加します。後の(より重要な)桁の列が、すでに処理された列に影響を与える方法はありません。このため、結果の桁のみ、または最初に引数の桁をゼロに設定しても違いはありません。
上記は常に正しいとは限りません。C++標準では、これを破る実装が許可されています。
そのようなDeathstation 9000 :-) は、OPが「32ビット符号なし整数を含むunsigned short
を意味する場合、33ビットint
を使用する必要があります。 「。 unsigned int
が意図されている場合、DS9Kは32ビットint
と、パディングビット付きの32ビットunsigned int
を使用する必要があります。 (符号なし整数は、§3.9.1/ 3に従って符号付きの整数と同じサイズにする必要があり、§3.9.1/ 1ではパディングビットが許可されています。)サイズとパディングビットの他の組み合わせも機能します。
私が知る限り、これはそれを破る唯一の方法です。
int
がソース型のすべての値を表すことができる場合にのみ許可されます(§4.5/ 1)。したがって、int
には、値に寄与する少なくとも32ビットと符号ビットが必要です。int
には32を超える値ビット(符号ビットをカウントしない)を含めることはできません。これは、加算がオーバーフローしないためです。あなたはすでに賢明な答えを持っています:符号なし算術はモジュロ算術であり、結果が保持されます、あなたは数学的にそれを証明することができます...
ただし、コンピューターの優れた点の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;
}
これは、32ビット空間でa
およびb
のすべての可能な値を列挙し、等式が成り立つかどうかをチェックします。動作しない場合は、動作しなかったケースを出力します。これは、健全性チェックとして使用できます。
そして、 Clangによる :平等が保持されます。
さらに、算術規則がビット幅に依存しない(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 もちろん、プログラムが不注意で未定義の動作を引き起こした場合、それはあまり証明されません。
簡単な答えは次のとおりです。両方の式は同等です
a
とb
は32ビット符号なし整数であるため、オーバーフローの場合でも結果は同じです。符号なし算術はこれを保証します:結果の符号なし整数型で表現できない結果は、結果の型で表現できる最大値よりも1大きい数を法として減少します。長い答えは次のとおりです。これらの表現が異なる既知のプラットフォームはありませんが、統合プロモーションのルールのために、標準はそれを保証しません。
a
およびb
(符号なし32ビット整数)の型のランクがint
よりも高い場合、計算は符号なし、2を法として実行されます。32、およびa
およびb
のすべての値の両方の式に対して同じ定義済みの結果を生成します。
逆に、a
とb
の型がint
よりも小さい場合、両方ともint
に昇格され、計算は符号付き算術を使用して実行されます。未定義の動作を呼び出します。
int
に少なくとも33の値ビットがある場合、上記の式はどちらもオーバーフローしないため、結果は完全に定義され、両方の式で同じ値になります。
int
の値が正確に32ビットである場合、canboth(---)式の計算canオーバーフロー、たとえば値a=0xFFFFFFFF
およびb=1
は、両方の式でオーバーフローを引き起こします。これを回避するには、((a & 255) + (b & 255)) & 255
と記述する必要があります。
幸いなことに、そのようなプラットフォームはありません1。
1 より正確には、そのような実際のプラットフォームは存在しませんが、 DS9K を設定して、そのような動作を示し、C標準に準拠することができます。
同一オーバーフローがないと仮定。どちらのバージョンもオーバーフローの影響をまったく受けませんが、ダブルとバージョンはオーバーフローに対してより耐性があります。私は、この場合のオーバーフローが問題になるシステムを認識していませんが、作成者がこれを実行している場合、これを行うのを見ることができます。
はい、算術で証明できますが、より直感的な答えがあります。
追加するとき、すべてのビットはそれ自体よりも重要なものにのみ影響します。それほど重要ではありません。
したがって、追加する前に上位ビットに何をしても、変更された最下位ビットよりも重要でないビットのみを保持する限り、結果は変わりません。
証拠は簡単で、読者のための演習として残されています
しかし、実際にこれを答えとして正当化するために、コードの最初の行は、b
**の最後の8ビット(b
のすべての上位ビットをゼロに設定)を取得し、これをa
そして、結果の最後の8ビットのみを取得し、すべての上位ビットをゼロに設定します。
2行目は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
はオーバーフローし、期待される結果を生成します。