14

次のコードを検討してください。

UInt32 val = 1;
UInt32 shift31 = val << 31;                    // shift31  == 0x80000000
UInt32 shift32 = val << 32;                    // shift32  == 0x00000001
UInt32 shift33 = val << 33;                    // shift33  == 0x00000002
UInt32 shift33a = (UInt32)((UInt64)val << 33); // shift33a == 0x00000000

(32 より大きいシフトの使用に関する) 警告は生成されないため、予期される動作である必要があります。

生成されたアセンブリに実際に出力されるコード (または少なくとも Reflector によるコードの解釈) は次のとおりです。

 uint val = 1;
 uint shift31 = val << 0x1f;
 uint shift32 = val;
 uint shift33 = val << 1;
 uint shift33a = val << 0x21;  

IL (ここでも Reflector を使用) は

L_0000: nop 
L_0001: ldc.i4.1 
L_0002: stloc.0 
L_0003: ldloc.0 
L_0004: ldc.i4.s 0x1f
L_0006: shl 
L_0007: stloc.1 
L_0008: ldloc.0 
L_0009: stloc.2 
L_000a: ldloc.0 
L_000b: ldc.i4.1 
L_000c: shl 
L_000d: stloc.3 
L_000e: ldloc.0 
L_000f: conv.u8 
L_0010: ldc.i4.s 0x21
L_0012: shl 
L_0013: conv.u4 
L_0014: stloc.s shift33a

が起こっているのか理解しています ( MSDNで説明されています)。コードがコンパイルされると、32 ビット値をシフトするときに下位 5 ビットのみが使用されます...なぜこれが起こるのか知りたいです。

shift33a出てくる方法は、ILのc#プレゼンテーションが別のものにコンパイルされるため、Reflectorで何かが正しくないと私に思わせます)

質問):

  • 「シフトする値」の下位 5 ビットのみが使用されるのはなぜですか ?
  • 「31 ビットを超えてシフトしても意味がない」場合、警告がないのはなぜですか?
  • これは下位互換性の問題ですか (つまり、これはプログラマーが「期待」するものですか)?
  • 基礎となる IL は 31 ビットを超えるシフトを行うことができますが (のようにL_0010: ldc.i4.s 0x21)、コンパイラが値をトリミングしているというのは正しいですか?
4

3 に答える 3

9

基本的には、x86 が算術シフト オペコードを処理する方法に要約されます。シフト カウントの下位 5 ビットのみを使用します。たとえば、 80386 プログラミング ガイドを参照してください。C/C++ では、(32 ビット整数の場合) 31 ビットを超えるビット シフトを行うことは、技術的に未定義の動作であり、「必要のないものにはお金を払わない」という C の哲学に従います。C99標準のセクション6.5.7、パラグラフ3から:

整数昇格は各オペランドで実行されます。結果の型は、昇格された左オペランドの型です。右オペランドの値が負であるか、プロモートされた左オペランドの幅以上である場合、動作は未定義です。

これにより、コンパイラは x86 でシフトのために単一のシフト命令を省略することができます。x86 では、1 つの命令で 64 ビット シフトを実行できません。これらは、SHLD / SHRD命令といくつかの追加ロジックを使用します。x86_64 では、1 つの命令で 64 ビットのシフトを実行できます。

たとえば、gcc 3.4.4 は、任意の量だけ 64 ビットの左シフトを行う次のアセンブリを発行します (でコンパイルされ-O3 -fomit-frame-pointerます)。

uint64_t lshift(uint64_t x, int r)
{
  return x << r;
}

_lshift:
    movl    12(%esp), %ecx
    movl    4(%esp), %eax
    movl    8(%esp), %edx
    shldl   %cl,%eax, %edx
    sall    %cl, %eax
    testb   $32, %cl
    je      L5
    movl    %eax, %edx
    xorl    %eax, %eax
L5:
    ret

さて、私は C# にあまり詳しくありませんが、C# にも同様の哲学があると思います。つまり、可能な限り効率的に実装できるように言語を設計するということです。シフト演算がシフト カウントの下位 5/6 ビットのみを使用するように指定することで、JIT コンパイラは可能な限り最適にシフトをコンパイルできます。32 ビット シフトは、64 ビット システムでの 64 ビット シフトと同様に、JIT を単一のオペコードにコンパイルできます。

ネイティブ シフト オペコードの動作が異なるプラットフォームに C# が移植された場合、実際にはパフォーマンスがさらに低下します。JIT コンパイラは、標準が尊重されていることを確認する必要があるため、ロジックをシフト カウントの下位 5/6 ビットのみが使用されていることを確認してください。

于 2009-03-13T21:44:41.137 に答える
3

Unit32 は、仕様で定義されている 32 ビットでオーバーフローします。何を期待していますか?

CLR は、オーバーフロー検出 operator(1) を使用した左シフトを定義しません。そのような施設が必要な場合は、自分で確認する必要があります。

(1) C# コンパイラはそれを long にキャストするかもしれませんが、よくわかりません。

于 2009-03-13T22:32:21.053 に答える
1

この簡単なテストを C (gcc、linux) で書いたところ、同様の結果が得られました。興味深いのは、オーバー シフトの定数定義がラップ アラウンドではなくゼロになったことです。それらについて警告を与えたので、少なくともそれが「間違った」ことであるという認識がありました.

#include <stdio.h>

unsigned int is0 = 1 << 31;
unsigned int is1 = 1 << 32;
unsigned int is2 = 1 << 33;

int main()
{
   unsigned int loopy = 0;
   int x = 0;
   printf("0x%08X\n", is0);
   printf("0x%08X\n", is1);
   printf("0x%08X\n", is2);


   for (x = 0; x < 35; ++x)
   {
      loopy = 1 << x;
      printf("%02d 0x%08X\n", x,loopy);
   }

   return 0;
}

結果は次のとおりです。

0x80000000
0x00000000
0x00000000
00 0x00000001
01 0x00000002
02 0x00000004
03 0x00000008
04 0x00000010
05 0x00000020
06 0x00000040
07 0x00000080
08 0x00000100
09 0x00000200
10 0x00000400
11 0x00000800
12 0x00001000
13 0x00002000
14 0x00004000
15 0x00008000
16 0x00010000
17 0x00020000
18 0x00040000
19 0x00080000
20 0x00100000
21 0x00200000
22 0x00400000
23 0x00800000
24 0x01000000
25 0x02000000
26 0x04000000
27 0x08000000
28 0x10000000
29 0x20000000
30 0x40000000
31 0x80000000
32 0x00000001
33 0x00000002
34 0x00000004
于 2009-03-13T22:04:13.947 に答える