เหตุใดพฤติกรรมของโค้ดจึงแตกต่างกันในโหมดรีลีสและดีบัก


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;
}

ผลลัพธ์ในโหมด Debug คือ 100,100,100,100,100 แต่ในโหมด Release จะเป็น: 100,100,100,100,0

เกิดอะไรขึ้น?

ได้รับการทดสอบโดยใช้. NET framework 4.7.1 และ .NET Core 2.0.0


คุณใช้ Visual Studio (หรือคอมไพเลอร์) เวอร์ชันใด
Styxxy

9
Repro; การเพิ่ม a Console.WriteLine(i);เข้าไปในลูปสุดท้าย ( dd[i] = d;) "แก้ไข" ซึ่งแนะนำข้อผิดพลาดของคอมไพเลอร์หรือ JIT bug; มองเข้าไปใน IL ...
Marc Gravell

@Styxxy ทดสอบบน vs2015 2017 และกำหนดเป้าหมายทุก. net framework> = 4.5
Ashkan Nourzadeh

บั๊กแน่นอน นอกจากนี้ยังจะหายไปหากคุณนำออกif (dd.Length >= N) return;ซึ่งอาจเป็นการทำซ้ำที่ง่ายกว่า
Jeroen Mostert

1
ไม่น่าแปลกใจที่เมื่อการเปรียบเทียบเป็น apples-to-apples แล้ว x64 codegen สำหรับ. Net Framework และ. Net Core จะมีประสิทธิภาพที่ใกล้เคียงกันเนื่องจาก (โดยค่าเริ่มต้น) จะเป็นรหัสสร้าง jit เดียวกัน การเปรียบเทียบประสิทธิภาพของ. Net Framework x86 codegen กับ x86 codegen ของ. Net Core จะน่าสนใจ (ซึ่งใช้ RyuJit ตั้งแต่ 2.0) ยังมีบางกรณีที่ผู้เฒ่าจิต (aka 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 

เทียบกับ

// ...
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

1
ฉันเห็นมันบน. NET เต็ม 4.7.1 เช่นกันดังนั้นถ้านี่ไม่ใช่ข้อผิดพลาดของ RyuJIT ฉันจะกินหมวกของฉัน
Jeroen Mostert

2
ฉันไม่สามารถสร้างซ้ำติดตั้ง. NET 4.7.1 และทำซ้ำได้ในขณะนี้
user3057557

3
@MarcGravell .Net framework 4.7.1 และ. net Core 2.0.0
Ashkan Nourzadeh

4
@AshkanNourzadeh ฉันอาจจะบันทึกที่นี่เพื่อความซื่อสัตย์โดยเน้นว่าผู้คนเชื่อว่าเป็นข้อผิดพลาดของ RyuJIT
Marc Gravell

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, jg 00007FF942690AE7 มันจะกระโดดกลับถ้า ebx (ซึ่งมี 4 ค่าสิ้นสุดของลูป) ใหญ่กว่า (jg) กว่า eax ค่า i สิ่งนี้จะล้มเหลวเมื่อเป็น 4 แน่นอนดังนั้นจึงไม่เขียนองค์ประกอบสุดท้ายในอาร์เรย์

มันล้มเหลวเพราะมันเป็นค่า register ของ i (eax ที่ 0x00007FF942690AF9) จากนั้นตรวจสอบด้วย 4 แต่ก็ยังต้องเขียนค่านั้น เป็นการยากที่จะระบุตำแหน่งที่แน่นอนของปัญหาเนื่องจากดูเหมือนว่าอาจเป็นผลมาจากการเพิ่มประสิทธิภาพของ (N-Old.Length) เนื่องจากบิวด์ดีบักมีโค้ดนั้น แต่บิวด์รีลีสคำนวณล่วงหน้า นั่นล่ะสำหรับคนที่คิดว่าจะแก้ไข;)


2
หนึ่งในวันนี้ฉันต้องใช้เวลาสักพักเพื่อเรียนรู้ opcodes แอสเซมบลี / CPU บางทีฉันก็คิดอย่างไร้เดียงสาว่า "ฉันฉันอ่านและเขียน IL ได้ - ฉันควรจะพูดมันได้" - แต่ฉันไม่เคยไปไหนมาไหน :)
Marc Gravell

x64 / x86 ไม่ใช่ภาษาแอสเซมบลีที่ดีที่สุดในการเริ่มต้นด้วย tho;) มันมี opcodes มากมายฉันเคยอ่านว่าไม่มีใครมีชีวิตอยู่ที่รู้จักพวกเขาทั้งหมด ไม่แน่ใจว่าจริงหรือเปล่า แต่ตอนแรกอ่านไม่ยาก แม้ว่าจะใช้รูปแบบง่ายๆไม่กี่อย่างเช่น [] ปลายทางก่อนส่วนต้นทางและสิ่งที่ลงทะเบียนค่าเฉลี่ยทั้งหมด (อัลคือส่วน 8 บิตของ rax, eax คือส่วน 32 บิตของ rax เป็นต้น) คุณสามารถก้าวผ่านมันไปได้ใน vs tho ซึ่งจะสอนคุณถึงสิ่งสำคัญ ฉันแน่ใจว่าคุณหยิบมันขึ้นมาอย่างรวดเร็วเพราะคุณรู้แล้วว่า IL opcodes;)
Frans Bouma
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.