52088

[code]

    int a = -2147483648;
    int b = a * -1; // -2147483648
    
[/code]

32ビットの signed int の値の範囲が

> -2,147,483,648 ～ 2,147,483,647

であることから、b の値が +2147483648 になり得ないことは分かります。  
ただ、-2147483648 になる理由が分かりません。  
C# だけでなく Java などでも同様のようです。  
仕様と言ってしまえば、それまでなのでしょうが、何か合理的な理由があるのでしょうか？

[code]

    a = (a * -1) * -1
    
[/code]

という式が成り立つようにするためでしょうか。


CC BY-SA 4.0
2019-01-18T13:37:31.217
52087
2019-11-15T08:15:48.093
3925
question
c#
int 型の変数 -2147483648 に -1 をかけると -2147483648 になるのはなぜか


数学的な意味ではなく、結果が桁あふれを起こしていて、それを下位32bit signed intとして  
見た場合の値が -2147483648 なのだと考えられます。

Windows10 の電卓ツールでプログラマモードにすると 64bit計算が出来ますので、それで  
確かめてみてください。

32bit signed int を 64bit signed int に拡張すると、-2147483648 は 0xFFFFFFFF80000000 に  
なります。 -1 は 0xFFFFFFFFFFFFFFFF で、掛けると結果は 0x0000000080000000 で、  
64bit演算では正の数の 2147483648 になりますが、答えを格納する領域が 32bit しかない  
ために、-2147483648 に見えるわけです。

あるいは、32bit 同士の演算だったとしても、0x80000000 と 0xFFFFFFFF を掛けると、  
結果は 0x7FFFFFFF80000000 になりますが、下位32bit は上記と同じ 0x80000000 で  
32bit signed int として見れば、-2147483648 になるわけです。


CC BY-SA 4.0
2019-01-18T14:09:27.360
52088
2019-01-18T14:09:27.360
26370
52087
answer


うーん、32bit x 32bit = 32bitの乗算の話は、単に64bitに拡張しても分かりやすいですが、そもそもどうして32bit x
32bitの結果が64bitで与えられないのでしょうか？それは繰り返し加減乗除算を適用したいからです。アセンブリ言語ではなく型付き言語の縛りです。

試しにC++でint x intの乗算命令をディスアセンブリしてみます。

[code]

    int mul(int a, int b) {
        return a * b;
    }
    
[/code]

結果はWindows 10(64bit版)、Visual Studio 2015でx64ビルドしたときのものです。

[code]

    int mul(int a, int b) {
    00007FF6588A1680 89 54 24 10          mov         dword ptr [rsp+10h],edx  
    00007FF6588A1684 89 4C 24 08          mov         dword ptr [rsp+8],ecx  
    00007FF6588A1688 55                   push        rbp  
    00007FF6588A1689 57                   push        rdi  
    00007FF6588A168A 48 81 EC C8 00 00 00 sub         rsp,0C8h  
    00007FF6588A1691 48 8B EC             mov         rbp,rsp  
    00007FF6588A1694 48 8B FC             mov         rdi,rsp  
    00007FF6588A1697 B9 32 00 00 00       mov         ecx,32h  
    00007FF6588A169C B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
    00007FF6588A16A1 F3 AB                rep stos    dword ptr [rdi]  
    00007FF6588A16A3 8B 8C 24 E8 00 00 00 mov         ecx,dword ptr [rsp+0E8h]  
        return a * b;
    00007FF6588A16AA 8B 85 E0 00 00 00    mov         eax,dword ptr [a]  
    00007FF6588A16B0 0F AF 85 E8 00 00 00 imul        eax,dword ptr [b]  
    }
    00007FF6588A16B7 48 8D A5 C8 00 00 00 lea         rsp,[rbp+0C8h]  
    00007FF6588A16BE 5F                   pop         rdi  
    00007FF6588A16BF 5D                   pop         rbp  
    00007FF6588A16C0 C3                   ret  
    
[/code]

64bitコードにおける関数の戻り値はRAXレジスタで渡します。戻り値の型はintなのでRAXの下位32bit、すなわちEAXが使用されます。ここのIMUL命令は32bit
x 32bit = 64bitの符号付き演算を行います。実際に行われている演算はa = [a31, a30, ... a1, a0], b = [b31,
b30, ... b1, b0]とすると、c = ((a31 + b31) = 1ならば-1、そうでなければ1) * Σ_{0<=i<=30,
0<=j<=30} (ai * bj * (2 ^ (i + j))です。つまりRAX = -2147483648 * -1 =
2147483648となります。ただし-2147483648 ≡ 2147483648 (mod
4294967296)なのでEAXは-2147483648となります。

これをもう少し具体化するためにC++の関数ポインタをキャストしてみましょう。

[code]

    int mul(int a, int b) {
        return a * b;
    }
    
    int main()
    {
        static_assert(sizeof(int) == 4, "ERROR");
        static_assert(sizeof(long long int) == 8, "ERROR");
        long long int (*mul64)(int a, int b);
        mul64 = (decltype(mul64))&mul;
        long long int res = mul64(-2147483648, -1);
        printf("%lld\n", res);
        printf("%d\n", (int)res);
        return 0;
    }
    
[/code]

実行結果は次の通りです。

[code]

    2147483648
    -2147483648
    
[/code]


CC BY-SA 4.0
2019-01-19T11:36:27.693
52118
2019-01-19T11:46:56.277
2019-01-19T11:46:56.277
31654
31654
52087
answer


-2147483648の2の補数が、同じバイナリ列になってしまうからです。

大抵のアーキテクチャにおいて、負の値は2の補数で表現します。  
そして、符号反転演算は2の補数を求めて算出する実装になっています。  
結果、ある値の2の補数が元の値と同じバイナリ列になってしまう値があります(0と-2147483648の二つ)。

この値については、符号反転演算をすると元の値になってしまうので、-1をかけても値が変わらないという現象が起きます。  
(0の2の補数は0、-2147483648の2の補数は-2147483648)


CC BY-SA 4.0
2019-11-14T06:16:48.533
60507
2019-11-14T06:16:48.533
36619
52087
answer


> -2147483648 になる理由が分かりません。

int 型の計算でオーバーフローしているからです。

数字を ２進数で表現すると

[code]

    <------------------------ long 型------------------------------>
                                    <-------------- int 型 -------->
    ‭1111111111111111111111111111111110000000000000000000000000000000‬ -2147483648
    ‭1111111111111111111111111111111110000000000000000000000000000001‬ -2147483647
    ‭1111111111111111111111111111111111111111111111111111111111111111‬ -1
    000000000000000000000000000000000‬0000000000000000000000000000000‬  0
    000000000000000000000000000000000‬0000000000000000000000000000001  1
    00000000000000000000000000000000‭01111111111111111111111111111110‬  2147483646
    ‭00000000000000000000000000000000‭01111111111111111111111111111111‬  2147483647
    ‭‭0000000000000000000000000000000010000000000000000000000000000000‬  2147483648
                                    <-------------- int 型 -------->
    <------------------------ long 型------------------------------>
    
[/code]

２の補数 という仕組みはうまくできており  
1)ビットの先頭桁が 1 の物はマイナスの値とする  
2)0 と -0 を作らず -1 は 全ビット 1 にする事で表現する。

の仕組みがあるため  
-1 に 1 を足すと 0 になる。

１０進で見ると当たり前ですが、2進で見ると  
`1111111111111111111111111111111`‬ -1 に 1 を足すと  
`0000000000000000000000000000000‬` 0 になる。

のは 符号なしで見たときに 一番上の桁の桁あふれを無視している計算と同じです。

また  
`01111111111111111111111111111111`‬ 2147483647 に １を足すと  
`10000000000000000000000000000000‬` -2147483648 になります。

桁あふれした結果 最上位 ビットが 1 になりマイナスの数字となるからです。

今回の計算結果 `2147483648` は  
`01111111111111111111111111111111`‬ 2147483647 に １を足した結果なので  
`10000000000000000000000000000000‬` の2進数になるのですが  
これは  
`10000000000000000000000000000000‬` -2147483648  
となります。

C# では checked で 囲むと int 型の計算のオーバーフローで  
System.OverflowException 例外が発生するようにできます。

[code]

    checked {
    int a = -2147483648;
    int b = a * -1; // -2147483648
    }
    
[/code]

コンピュータの数値計算では 桁あふれを事前に考慮してプログラミングが必要です。  
日頃から桁あふれを考慮して プログラムをするのは大事です。


CC BY-SA 4.0
2019-11-15T08:15:48.093
60551
2019-11-15T08:15:48.093
18851
52087
answer