web-dev-qa-db-ja.com

「k + = c + = k + = c;」にインライン演算子の説明はありますか?

次の操作の結果の説明は何ですか?

k += c += k += c;

次のコードからの出力結果を理解しようとしました:

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

現在、私は "k"の結果が80である理由を理解するのに苦労しています。なぜk = 40を割り当てても機能しないのですか(実際、Visual Studioはその値が他の場所で使用されていないことを通知しています)?

なぜkではなく110なのですか?

操作を次のように分割した場合:

k+=c;
c+=k;
k+=c;

結果はk = 110です。

[〜#〜] cil [〜#〜] を調べようとしていましたが、生成されたCILの解釈はそれほど深くなく、いくつかの詳細を取得できません。

 // [11 13 - 11 24]
IL_0001: ldc.i4.s     10
IL_0003: stloc.0      // k

// [12 13 - 12 24]
IL_0004: ldc.i4.s     30
IL_0006: stloc.1      // c

// [13 13 - 13 30]
IL_0007: ldloc.0      // k expect to be 10
IL_0008: ldloc.1      // c
IL_0009: ldloc.0      // k why do we need the second load?
IL_000a: ldloc.1      // c
IL_000b: add          // I expect it to be 40
IL_000c: dup          // What for?
IL_000d: stloc.0      // k - expected to be 40
IL_000e: add
IL_000f: dup          // I presume the "magic" happens here
IL_0010: stloc.1      // c = 70
IL_0011: add
IL_0012: stloc.0      // k = 80??????
88

a op= b;のような操作はa = a op b;と同等です。割り当てはステートメントまたは式として使用できますが、式として割り当てられた値を生成します。あなたの声明...

k += c += k += c;

...代入演算子は右結合なので、次のように書くこともできます

k += (c += (k += c));

または(拡張)

k =  k +  (c = c +  (k = k  + c));
     10    →   30    →   10 → 30   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   40 ← 10 + 30   // operator evaluation
      ↓   70 ← 30 + 40
80 ← 10 + 70

評価全体で、関連する変数の古い値が使用されます。これは特にkの値に当てはまります(以下のILのレビューと link Wai Ha Leeの提供を参照してください)。したがって、70 + 40(kの新しい値)= 110ではなく、70 + 10(kの古い値)= 80になります。

重要なのは、(C#に従って spec "式のオペランドは左から右に評価される"(オペランドは、変数cおよびk(この場合)です。これは、この場合は右から左への実行順序を決定する演算子の優先順位と結合性には依存しません。 (このページのEric Lippertの answer へのコメントを参照してください)。


次に、ILを見てみましょう。 ILはスタックベースの仮想マシンを想定しています。つまり、レジスタを使用しません。

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

スタックは次のようになります(左から右、スタックの上部が右)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

IL_000c: dupIL_000d: stloc.0、つまりkへの最初の割り当ては、最適化して削除できることに注意してください。おそらく、ILをマシンコードに変換するときに、ジッターによって変数に対してこれが行われます。

また、計算に必要なすべての値は、割り当てが行われる前にスタックにプッシュされるか、これらの値から計算されることに注意してください。 (stlocによって)割り当てられた値は、この評価中に再利用されることはありません。 stlocはスタックの最上部をポップします。


次のコンソールテストの出力は(最適化をオンにしたReleaseモード)です

kの評価(10)
cを評価しています(30)
kの評価(10)
cを評価しています(30)
kに割り当てられた40
70をcに割り当て
kに割り当てられた80

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

まず、ヘンクとオリビエの答えは正しいです。少し違う方法で説明したいと思います。具体的には、この点についてお話ししたいと思います。次のステートメントセットがあります。

int k = 10;
int c = 30;
k += c += k += c;

そして、次のステートメントのセットと同じ結果が得られるはずであると誤って結論付けます。

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

どのようにしてそれを間違ったのか、そしてそれを正しく行う方法を知ることは有益です。それを分解する正しい方法はこのようなものです。

まず、最も外側の+ =を書き換えます

k = k + (c += k += c);

次に、最も外側の+を書き換えます。 x = y + zは常に「yを一時的に評価し、zを一時的に評価し、一時を合計し、合計をxに割り当てる」と同じであることに同意してください。それで、それを非常に明示的にしましょう:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

これが間違ったステップなので、明確であることを確認してください。複雑な操作をより単純な操作に分解する場合は、ゆっくりと慎重に行う必要がありますおよびステップをスキップしないでください。手順をスキップすると、間違いが発生します。

OK、今度は、t2への割り当てをゆっくりと慎重に分解します。

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

この割り当てでは、cに割り当てられているのと同じ値がt2に割り当てられるため、次のようにしましょう。

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

すごい。次に、2行目を分解します。

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

すばらしい、私たちは進歩しています。 t4への割り当てを分解します。

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

次に3行目を分解します。

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

そして、全体を見ることができます:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

完了したら、kは80、cは70です。

次に、これがILでどのように実装されているかを見てみましょう。

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

これは少しトリッキーです:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

上記を次のように実装することもできます

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

"dup"トリックを使用しているのは、コードが短くなり、ジッターが軽減され、同じ結果が得られるためです。 一般に、C#コードジェネレーターは、一時的な「エフェメラル」を可能な限りスタック上に維持しようとします。エフェメラルの数が少ない方がILを追跡しやすい場合は、最適化を有効にします off 、そしてコードジェネレーターはそれほど積極的ではありません。

ここで、cを取得するために同じトリックを実行する必要があります。

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

そして最後に:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

それ以外には合計が必要ないため、重複しません。これでスタックは空になり、ステートメントの終わりです。

この話の教訓は次のとおりです。複雑なプログラムを理解しようとしているときは、常に1つずつ操作を分解してください近道をしないでください。彼らはあなたを迷わせます。

25
Eric Lippert

つまり、最初の+=は、元のkに適用されますか、それとも、より右側で計算された値に適用されますか?

答えは、割り当ては右から左にバインドされますが、操作は依然として左から右に進むということです。

したがって、左端の+=10 += 70を実行しています。

14
Henk Holterman

私はgccとpgccで例を試し、110を取得しました。それらが生成したIRを確認したところ、コンパイラはexprを次のように拡張しました。

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

それは私には合理的に見えます。

0
Brian Yang