リリースモードとデバッグモードでコードの動作が異なるのはなぜですか?


84

次のコードについて考えてみます。

private static void Main(string[] args)
{
    var ar = new double[]
    {
        100
    };

    FillTo(ref ar, 5);
    Console.WriteLine(string.Join(",", ar.Select(a => a.ToString()).ToArray()));
}

public static void FillTo(ref double[] dd, int N)
{
    if (dd.Length >= N)
        return;

    double[] Old = dd;
    double d = double.NaN;
    if (Old.Length > 0)
        d = Old[0];

    dd = new double[N];

    for (int i = 0; i < Old.Length; i++)
    {
        dd[N - Old.Length + i] = Old[i];
    }
    for (int i = 0; i < N - Old.Length; i++)
        dd[i] = d;
}

デバッグモードでの結果は、100、100、100、100、100です。ただし、リリースモードでは、100,100,100,100,0です。

何が起こっている?

.NET Framework4.7.1および.NETCore2.0.0を使用してテストされました。


どのバージョンのVisualStudio(またはコンパイラ)を使用していますか?
Styxxy 2017

9
再現; Console.WriteLine(i);最後のループにaを追加すると(dd[i] = d;)が「修正」され、コンパイラのバグまたはJITのバグが示唆されます。IL ...に探して
マルクGravell

@ Styxxy、vs2015、2017でテストされ、すべての.netフレームワークを対象> = 4.5
Ashkan Nourzadeh 2017

間違いなくバグです。を削除するif (dd.Length >= N) return;と消えますが、これはより簡単な再現かもしれません。
Jeroen Mostert 2017

1
比較がアップルトゥアップルになると、.NetFrameworkと.NetCoreのx64codegenが(デフォルトで)本質的に同じjit生成コードであるため、同様のパフォーマンスを発揮することは驚くべきことではありません。.Net Framework x86codegenのパフォーマンスを.NetCoreのx86codegen(2.0以降RyuJitを使用)と比較するのは興味深いことです。古いjit(別名Jit32)がRyuJitが知らないいくつかのトリックを知っている場合がまだあります。そして、そのようなケースを見つけた場合は、CoreCLRリポジトリでそれらの問題を開いてください。
Andy Ayers

回答:


70

これはJITのバグのようです。私はテストしました:

// ... existing code unchanged
for (int i = 0; i < N - Old.Length; i++)
{
    // Console.WriteLine(i); // <== comment/uncomment this line
    dd[i] = d;
}

Console.WriteLine(i)修正を追加すると修正されます。唯一のILの変更は次のとおりです。

// ...
L_0040: ldc.i4.0 
L_0041: stloc.3 
L_0042: br.s L_004d
L_0044: ldarg.0 
L_0045: ldind.ref 
L_0046: ldloc.3 
L_0047: ldloc.1 
L_0048: stelem.r8 
L_0049: ldloc.3 
L_004a: ldc.i4.1 
L_004b: add 
L_004c: stloc.3 
L_004d: ldloc.3 
L_004e: ldarg.1 
L_004f: ldloc.0 
L_0050: ldlen 
L_0051: conv.i4 
L_0052: sub 
L_0053: blt.s L_0044
L_0055: ret 

vs

// ...
L_0040: ldc.i4.0 
L_0041: stloc.3 
L_0042: br.s L_0053
L_0044: ldloc.3 
L_0045: call void [System.Console]System.Console::WriteLine(int32)
L_004a: ldarg.0 
L_004b: ldind.ref 
L_004c: ldloc.3 
L_004d: ldloc.1 
L_004e: stelem.r8 
L_004f: ldloc.3 
L_0050: ldc.i4.1 
L_0051: add 
L_0052: stloc.3 
L_0053: ldloc.3 
L_0054: ldarg.1 
L_0055: ldloc.0 
L_0056: ldlen 
L_0057: conv.i4 
L_0058: sub 
L_0059: blt.s L_0044
L_005b: ret 

これは正確に正しく見えます(唯一の違いは、余分なldloc.3と、、およびcall void [System.Console]System.Console::WriteLine(int32)異なるが同等のターゲットですbr.s)。

JITの修正が必要だと思います。

環境:

  • Environment.Version:4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS:15.5.0プレビュー5.0
  • dotnet --version:2.1.1

では、どこでバグを報告しますか?
Ashkan Nourzadeh 2017

1
.NETフル4.7.1でも見られるので、これがRyuJITのバグでなければ、帽子をかぶってしまいます。
Jeroen Mostert 2017

2
.NET 4.7.1をインストールして再現できず、再現できるようになりました。
user3057557 2017

3
@ MarcGravell.Netフレームワーク4.7.1および
.netCore

4
@AshkanNourzadeh正直に言うと、おそらくここにログを記録し、人々はそれがRyuJITエラーであると信じていることを強調します
MarcGravell

6

確かにアセンブリエラーです。x64、.net 4.7.1、リリースビルド。

分解:

            for(int i = 0; i < N - Old.Length; i++)
00007FF942690ADD  xor         eax,eax  
            for(int i = 0; i < N - Old.Length; i++)
00007FF942690ADF  mov         ebx,esi  
00007FF942690AE1  sub         ebx,ebp  
00007FF942690AE3  test        ebx,ebx  
00007FF942690AE5  jle         00007FF942690AFF  
                dd[i] = d;
00007FF942690AE7  mov         rdx,qword ptr [rdi]  
00007FF942690AEA  cmp         eax,dword ptr [rdx+8]  
00007FF942690AED  jae         00007FF942690B11  
00007FF942690AEF  movsxd      rcx,eax  
00007FF942690AF2  vmovsd      qword ptr [rdx+rcx*8+10h],xmm6  
            for(int i = 0; i < N - Old.Length; i++)
00007FF942690AF9  inc         eax  
00007FF942690AFB  cmp         ebx,eax  
00007FF942690AFD  jg          00007FF942690AE7  
00007FF942690AFF  vmovaps     xmm6,xmmword ptr [rsp+20h]  
00007FF942690B06  add         rsp,30h  
00007FF942690B0A  pop         rbx  
00007FF942690B0B  pop         rbp  
00007FF942690B0C  pop         rsi  
00007FF942690B0D  pop         rdi  
00007FF942690B0E  pop         r14  
00007FF942690B10  ret  

問題はアドレス00007FF942690AFD、jg00007FF942690AE7にあります。ebx(4、ループ終了値を含む)がeax、値iよりも大きい(jg)場合、ジャンプバックします。もちろんこれは4の場合は失敗するため、配列の最後の要素は書き込まれません。

iのレジスタ値(eax、0x00007FF942690AF9)を含み、4でチェックするため失敗しますが、それでもその値を書き込む必要があります。デバッグビルドにはそのコードが含まれているため、(N-Old.Length)の最適化の結果である可能性があるため、問題がどこにあるかを正確に特定するのは少し難しいですが、リリースビルドはそれを事前に計算します。だから、それはジャストインタイムの人々が修正するためのものです;)


2
最近のある日、私はアセンブリ/ CPUオペコードを学ぶために時間を割く必要があります。おそらく素朴に私は「ILを読み書きできる-私はそれを理解できるはずだ」と考え続けます-しかし私はそれを回避することは決してありません:)
MarcGravell

x64 / x86は、最初から最高のアセンブリ言語ではありません;)オペコードが非常に多いので、それらすべてを知っている人は誰もいないと読んだことがあります。それが本当かどうかはわかりませんが、最初はそれほど簡単に読むことはできません。[]、ソース部分の前の宛先、およびこれらのレジスタがすべて意味するものなど、いくつかの単純な規則を使用しますが(alはraxの8ビット部分、eaxはraxの32ビット部分など)。あなたはそれをvsthoでステップスルーすることができ、それはあなたに本質を教えるはずです。ILオペコードをすでに知っているので、すぐに手に入れることができると確信しています;)
Frans Bouma 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.