2011-06-12 15 views
3

64ビットマシン上で動作するタイプbyte*の大きなメモリブロックに対してポインタ演算を行う安全でないC#コードがあります。ほとんどの場合正しく動作しますが、物事が大きくなると、ポインタが正しくないところで何らかの破損が発生することがよくあります。C#での64ビットポインタ演算、算術オーバーフローの変更の動作の確認

奇妙なことは、「算術オーバーフロー/アンダーフローを確認する」をオンにすると、すべて正しく動作することです。私はオーバーフローの例外はありません。しかし、大きなパフォーマンスのため、私はこのオプションを使わないでコードを実行する必要があります。

この動作の違いを引き起こす原因は何ですか?

+6

あなたのコードの一部を表示できますか? – aL3891

+0

オフセットが符号付きで、ポインタが符号なしですか?負のオフセットはめったに必要ありません。 –

+0

コードの関連する部分は次のようになります。this.currentLocation + = sizeof(byte)、this.currentLocation + = numberOfChildren * sizeof(ushort)、this.currentLocation + = subTree.Lengthなど負のオフセットはありません。 –

答えて

3

これはC#コンパイラのバグです(filed on Connect)。 @Grant has shownは、C#コンパイラによって生成されたMSILが符号付きとしてuintオペランドを解釈することを示します。

18.5.6ポインタ演算安全でない状況において

+及び-演算子(§7.8.4:それはここに関連するセクション(18.5.6)がだ、C#の仕様に応じて間違っていますおよび§7.8.5)は、void*を除くすべてのポインタ型の値に適用できます。このように、すべてのポインタ型T*ために、次の演算子が暗黙的に定義されています。ポインタ型T*とタイプintの発現Nuintlong、またはulongの発現P、表現を考える

T* operator +(T* x, int y); 
T* operator +(T* x, uint y); 
T* operator +(T* x, long y); 
T* operator +(T* x, ulong y); 
T* operator +(int x, T* y); 
T* operator +(uint x, T* y); 
T* operator +(long x, T* y); 
T* operator +(ulong x, T* y); 
T* operator –(T* x, int y); 
T* operator –(T* x, uint y); 
T* operator –(T* x, long y); 
T* operator –(T* x, ulong y); 
long operator –(T* x, T* y); 

P + NN + PPで指定されたアドレスにN * sizeof(T)を追加したタイプT*のポインタ値を計算します。同様に、式P - Nは、Pで指定されたアドレスからN * sizeof(T)を引いたタイプT*のポインタ値を計算します。

ポインタ型T*の二つの表現、PQ、与えられ、式P – QPQによって与えられたアドレスとの間の差を計算し、次いでsizeof(T)によってその差を分割します。結果のタイプは常にlongです。事実上、P - Q((long)(P) - (long)(Q))/sizeof(T)と計算されます。

ポインタの算術演算がポインタ型の領域をオーバーフローした場合、結果は実装定義の方法で切り捨てられますが、例外は生成されません。


あなたはポインタにuintの追加を許可している、何の暗黙の型変換は行われません。そして、操作はポインタ型の領域をオーバーフローさせません。したがって切り詰めは許可されません。

+0

非常に興味深い!私のコードがうまくいかなかったときの最初の考えから「コンパイラのバグでなければならない」というのはしばらくありました;) –

3

安全でないコードを再確認してください。割り当てられたメモリブロックの外にメモリを読み書きすると、その「破損」が発生します。

+0

もちろん、そうです。私はこのようなコードを何年も書いています(C++でも)。新しいものは64ビットポインタで作業しており、オーバーフロー例外を発生させることなく、オーバーフローの変更の動作をチェックすることがわかります。 –

+0

この問題の点では、x86とx64に違いはありません。誤って1バイトだけが無効なポインタによって読み書きされ、別の場所で問題がポップアップします。エラーが発生した主なアルゴリズムよりも、安全ではないコードを確認してください。 – HandMadeOX

+0

私は同意します。アルゴリズムが正しく動作するとき、ポインタは決して無効ではありません。これは(64ビット関連の)ポインタ算術の問題の症状に過ぎません。とにかく、私は問題を解決し、それを示す短いコードを書いた。数時間後に回答として投稿します。 –

1

私は問題を解決してきたように自分の質問に答えるんだけど、まだuncheckedcheckedなぜ動作の変更についてのコメントを読んで興味があると思います。

このコードは、問題だけでなく、解決策を示しています(常にキャストを追加する前にlongにオフセット):

public static unsafe void Main(string[] args) 
{ 
    // Dummy pointer, never dereferenced 
    byte* testPtr = (byte*)0x00000008000000L; 

    uint offset = uint.MaxValue; 

    unchecked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    } 

    checked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    } 

    unchecked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + (long)offset)); 
    } 

    checked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + (long)offset)); 
    } 
} 

(64ビットマシン上で実行した場合)これが返されます:

7ffffff 
107ffffff 
107ffffff 
107ffffff 

(私のプロジェクトでは、この安全でないポインタ算術の不自然さを除いてすべてのコードをマネージコードとして最初に書きましたが、あまりにも多くのメモリを使用していたことがわかりました。これは単なるホビープロジェクトです。爆破は私です。)

+0

これが関連しているかどうかはわかりませんが、最近[x64 JITのバグ]が見つかりました(https://connect.microsoft.com/VisualStudio/feedback/details/674232/jit-optimizer-error-when -loop-controlling-variable-approaches-int-maxvalue)は、チェックされた算術演算が無効になったときに整数ラップアラウンドのために値が失われるInt32の範囲外の定数になります。私は、このテストケースに言及したそのバグにコメントを投稿します。 –

+1

[OK]を、@グラントの答えを参照してください。彼はJITを担当するMSエンジニアであり、何が起きているのかは間違いなく知っています。どうやら、C#コンパイラによって生成されたMSILはすでに "間違った"動作をしているので、JITバグではありません。私はC#の仕様を見て、C#コンパイラがあなたのコードからそのMSILを生成することができるかどうかを調べるつもりです。 –

+0

あなたの洞察力のあるコメントをありがとう! –

6

ここでチェックとチェックされていない点の違いは、実際ILのバグか悪いソースコードです(私は言語の専門家ではないので、C#コンパイラが正しい曖昧なソースコードについてはILを参照してください)。 4.0.30319.1バージョンのC#コンパイラを使ってこのテストコードをコンパイルしました(ただし、2.0の検証でも同じことが起こりました)。私が使用したコマンドラインオプションは/ o +/unsafe/debug:pdbonlyでした。未確認のブロックについては

、我々は、このILコードを持っている:ILで

//000008:  unchecked 
//000009:  { 
//000010:   Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    IL_000a: ldstr  "{0:x}" 
    IL_000f: ldloc.0 
    IL_0010: ldloc.1 
    IL_0011: add 
    IL_0012: conv.u8 
    IL_0013: box  [mscorlib]System.Int64 
    IL_0018: call  void [mscorlib]System.Console::WriteLine(string, 
                   object) 

はアドオンが2つのオペランド、byte型の1 *とタイプUINT32の他を取得し、11を相殺しました。 CLI仕様によれば、これらは実際にはそれぞれintとint32に標準化されています。 CLI仕様(正確にはパーティションIII)によると、結果はネイティブintになります。したがって、secodnオペランドは、ネイティブint型に昇格させる必要があります。仕様によれば、これは符号拡張を介して達成される。したがって、uint.MaxValue(符号付き表記では0xFFFFFFFFまたは-1)は符号が0xFFFFFFFFFFFFFFFFに拡張されます。次に、2つのオペランドが追加されます(0x0000000008000000L +(-1L)= 0x0000000007FFFFFFL)。 convオペコードは、ネイティブintをint64に変換するための検証目的でのみ必要です。int64は、生成されたコードではnopです。

は今すぐチェックをブロックするために、我々は、このILを持っている:

//000012:  checked 
//000013:  { 
//000014:   Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    IL_001d: ldstr  "{0:x}" 
    IL_0022: ldloc.0 
    IL_0023: ldloc.1 
    IL_0024: add.ovf.un 
    IL_0025: conv.ovf.i8.un 
    IL_0026: box  [mscorlib]System.Int64 
    IL_002b: call  void [mscorlib]System.Console::WriteLine(string, 
                   object) 

それはアドオンとCONVオペコードを除き、実質的に同一です。オペコードを追加するために、2つの '接尾辞'を追加しました。最初のものは明白な意味を持つ ".ovf"接尾辞です:オーバーフローをチェックしますが、2番目の接尾辞: ".un"も有効にする必要があります。 (すなわち、「add.un」はなく、「add.ovf.un」のみである)。 ".un"には2つの効果があります。最も明白なのは、オペランドが符号なし整数であるかのように、オーバーフローチェックが追加で行われることです。私たちのCSクラスの方法からは、2の補数バイナリエンコーディングのおかげで、符号付き加算と符号なし加算が同じであることを覚えているでしょうから、 ".un"は実際にオーバーフローチェックにのみ影響します。

間違っています。

ILスタックには2つの64ビット数値がないことに注意してください。int32とネイティブint(正規化後)があります。まあ、 ".un"は、int32からネイティブへの変換が上記のようにデフォルトの "conv.i"ではなく "conv.u"のように扱われることを意味します。したがって、uint.MaxValueはゼロで0x00000000FFFFFFFFLに拡張されます。その後、addは0x0000000107FFFFFFLを正しく生成します。コンボオペコードは、符号なしのオペランドを符号付きint64として表現できることを保証します。

あなたの修正プログラムは、64ビットで検索されます。 ILレベルでは、uint32オペランドをネイティブintまたはunsigned native intに明示的に変換することにより、32ビットと64ビットの両方でcheckとuncheckedが同じになります。

+0

ありがとう!問題が発生したときにILを勉強する時間を取っておくべきだったと思いますが、その時点で私はコードを動作させたいだけでした。私は現実的な使用で数GBのメモリが必要なので、このプロジェクトは「64ビットのみ」と考えています。 (私は144GBのRAMを持つサーバーを買う余裕がありました) –