9

C#4.0を使用しています。Visual Studio の「コードの最適化」がオンになっています。

クラスで次のコードを検討してください。

Dictionary<int, int> dictionary = new Dictionary<int, int>();

public void IncrementDictionary(int key) {
    if (!dictionary.ContainsKey(key)) {
        dictionary[key] = 1;
    } else {
        dictionary[key]++;
    }
}

ここで、 への呼び出しIncrementDictionaryは次の 2 つのいずれかを行います。

  • の値が存在しない場合key、値が作成され、1 に初期化されます。
  • 値が存在する場合、値は 1 ずつ増加します。

ILSpy を使用して結果を逆コンパイルするとどうなるか見てみましょう。

Dictionary<int, int> dictionary = new Dictionary<int, int>();

public void IncrementDictionary(int key) {
    if (!dictionary.ContainsKey(key)) {
        dictionary[key] = 1;
        return;
    }
    Dictionary<int, int> dictionary2;
    (dictionary2 = dictionary)[key] = dictionary2[key] + 1;
}

注:これを使用する実際の製品コードでは、オプティマイザ/コンパイラも作成:を作成し、最終行int key2 = key;で使用します。key2

わかりました。期待どおりのvarに置き換えられましたDictionary<int, int>。そしてステートメントは、 を使用する代わりにifを追加するように単純化されました。returnelse

しかし、一体なぜ、作成された元の辞書への新しい参照が作成されたのでしょうか?

4

2 に答える 2

11

次のような競合状態を回避することができると思います。

dictionary[i] = dictionary[i] + 1

アトミックではありません。値を取得してインクリメントした後、割り当てdictionary先が変わる可能性があります。

次のコードを想像してください。

public Dictionary<int, int> dictionary = new Dictionary<int, int>();

public void Increment()
{
    int newValue = dictionary[0] + 1;
    //meanwhile, right now in another thread: dictionary = new Dictionary<int, int>();
    dictionary[0] = newValue; //at this point, "dictionary" is actually pointing to a whole new instance
}

彼らが持っているローカル変数の割り当てにより、条件を回避するために次のようになります。

public void IncrementFix()
{
    var dictionary2 = dictionary;
    //in another thread: dictionary = new Dictionary<int, int>();
    //this is OK, dictionary2 is still pointing to the ORIGINAL dictionary
    int newValue = dictionary2[0] + 1;
    dictionary2[0] = newValue;
}

すべてのスレッドセーフ要件を完全に満たすわけではないことに注意してください。たとえば、この場合、値のインクリメントを開始しますがdictionary、クラスの参照はまったく新しいインスタンスに変更されています。ただし、より高いレベルのスレッド セーフが必要な場合は、独自の積極的な同期/ロックを実装する必要がありますが、これは通常、コンパイラの最適化の範囲外です。ただし、これは、私が知る限り、実際には処理に大きな影響を与えることはなく (もしあれば)、この状態を回避します。dictionaryこれは、プロパティの場合に特に当てはまる可能性がありますあなたの例のようなフィールドではありません。その場合、プロパティゲッターを2回解決しないことは間違いなく最適化です。(ひょっとして、実際のコードは、投稿したフィールドではなく、辞書のプロパティを使用していますか?)

編集:まあ、簡単な方法の場合:

public void IncrementDictionary() 
{
    dictionary[0]++;
}

LINQPad から報告される IL は次のとおりです。

IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldfld       UserQuery.dictionary
IL_0007:  dup         
IL_0008:  stloc.0     
IL_0009:  ldc.i4.0    
IL_000A:  ldloc.0     
IL_000B:  ldc.i4.0    
IL_000C:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.Int32>.get_Item
IL_0011:  ldc.i4.1    
IL_0012:  add         
IL_0013:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.Int32>.set_Item
IL_0018:  nop         
IL_0019:  ret         

完全にはわかりませんが (私は IL ウィズではありません)、dup呼び出しは基本的にスタック上の同じ辞書参照を 2 倍にするので、get 呼び出しと set 呼び出しの両方が同じ辞書を指すことに関係なく、そう思います。おそらくこれは、ILSpy が C# コードとして表現する方法です (少なくとも動作に関しては、多かれ少なかれ同じです)。おもう。私が言ったように、私はまだ私の手の甲のようにILを知らないので、私が間違っている場合は修正してください.

編集: 実行する必要がありますが、その最終的な要点は次のとおりです:+++=はアトミック操作ではなく、実際には C# で示されているよりも実行される命令の方がかなり複雑です。そのため、get/increment/set の各ステップが同じディクショナリ インスタンスで実行されることを確認するために (C# コードから期待および要求されるように)、ディクショナリへのローカル参照が作成され、フィールドの実行が回避されます。 get" 操作を 2 回実行すると、新しいインスタンスが指される可能性があります。ILSpy は、インデックス付き += 操作に関連するすべての作業がそれ次第であることを示しています。

于 2012-06-27T22:58:41.167 に答える
1

あなたの編集はあなたが表示しようとしていたものを台無しにしましたがdictionary[key]++、一時的なコピーを作成する理由は、インデックス付きゲッターが field を変更するdictionaryかどうかを知る方法がないためです。仕様は、インデックス付きの get 中にフィールドが変更された場合でも、インデックス付きの put が同じオブジェクトに対して実行されることを示しています。Dictionary<int,int>dictionarydictionary

ところで、.netは、クラスがプロパティをref. DictionaryメソッドActOnValue(TKey key, ActionByRef<TValue> action)andを提供することは可能です(パラメータは宣言されているが、同様に宣言されているとActOnValue<TParam>(TKey key, ActionByRef<TValue, TParam> action, ref TParam param)仮定します)。その場合、コレクションに 2 回インデックスを付けることなく、コレクション オブジェクトに対して読み取り-変更-書き込みを実行できます。残念ながら、そのような方法でプロパティを公開するための標準的な規則はなく、言語サポートもありません。ActionByRef<T>Action<T>refActionByRef<T1,T2>

于 2012-06-27T23:26:44.570 に答える