.NET JIT 잠재적 오류?


404

다음 코드는 Visual Studio에서 릴리스를 실행하고 Visual Studio 외부에서 릴리스를 실행할 때 다른 출력을 제공합니다. Visual Studio 2008을 사용하고 .NET 3.5를 대상으로합니다. .NET 3.5 SP1도 시도했습니다.

Visual Studio 외부에서 실행할 때 JIT가 시작됩니다. (a) C #과 관련하여 미묘한 부분이 있거나 (b) JIT에 실제로 오류가 있습니다. 나는 JIT가 잘못 될 수 있다고 의심하지만 다른 가능성이 부족합니다 ...

Visual Studio에서 실행할 때 출력 :

    0 0,
    0 1,
    1 0,
    1 1,

Visual Studio 외부에서 릴리스를 실행할 때 출력 :

    0 2,
    0 2,
    1 2,
    1 2,

이유가 무엇입니까?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    struct IntVec
    {
        public int x;
        public int y;
    }

    interface IDoSomething
    {
        void Do(IntVec o);
    }

    class DoSomething : IDoSomething
    {
        public void Do(IntVec o)
        {
            Console.WriteLine(o.x.ToString() + " " + o.y.ToString()+",");
        }
    }

    class Program
    {
        static void Test(IDoSomething oDoesSomething)
        {
            IntVec oVec = new IntVec();
            for (oVec.x = 0; oVec.x < 2; oVec.x++)
            {
                for (oVec.y = 0; oVec.y < 2; oVec.y++)
                {
                    oDoesSomething.Do(oVec);
                }
            }
        }

        static void Main(string[] args)
        {
            Test(new DoSomething());
            Console.ReadLine();
        }
    }
}

8
예-어떻습니까 : .Net JIT만큼 중요한 버그를 발견 한 것을 축하합니다!
Andras Zoltan

73
이것은 x86에서 4.0 프레임 워크의 12 월 9 일 빌드에서 재현 한 것으로 보입니다. 지터 팀에 전달하겠습니다. 감사!
Eric Lippert

28
이것은 실제로 금 배지를받을 가치가있는 몇 안되는 질문 중 하나입니다 .
Mehrdad Afshari

28
우리 모두 가이 질문에 관심이 있다는 사실 은 .NET JIT의 버그는 기대 하지 않습니다 .
이안 링 로즈

2
우리는 모두 Microsoft가 걱정스럽게 대답하기를 기다리고 있습니다 .....
Talha

답변:


211

JIT 최적화 프로그램 버그입니다. 내부 루프를 풀지 만 oVec.y 값을 올바르게 업데이트하지 않습니다.

      for (oVec.x = 0; oVec.x < 2; oVec.x++) {
0000000a  xor         esi,esi                         ; oVec.x = 0
        for (oVec.y = 0; oVec.y < 2; oVec.y++) {
0000000c  mov         edi,2                           ; oVec.y = 2, WRONG!
          oDoesSomething.Do(oVec);
00000011  push        edi  
00000012  push        esi  
00000013  mov         ecx,ebx 
00000015  call        dword ptr ds:[00170210h]        ; first unrolled call
0000001b  push        edi                             ; WRONG! does not increment oVec.y
0000001c  push        esi  
0000001d  mov         ecx,ebx 
0000001f  call        dword ptr ds:[00170210h]        ; second unrolled call
      for (oVec.x = 0; oVec.x < 2; oVec.x++) {
00000025  inc         esi  
00000026  cmp         esi,2 
00000029  jl          0000000C 

oVec.y를 4로 증가 시키면 버그가 사라져서 너무 많은 호출이 풀립니다.

한 가지 해결 방법은 다음과 같습니다.

  for (int x = 0; x < 2; x++) {
    for (int y = 0; y < 2; y++) {
      oDoesSomething.Do(new IntVec(x, y));
    }
  }

업데이트 : 2012 년 8 월에 다시 확인 된이 버그는 버전 4.0.30319 지터에서 수정되었습니다. 그러나 v2.0.50727 지터에는 여전히 존재합니다. 오랜만에 구버전에서는이 문제를 해결하지 못할 것 같습니다.


3
+1, 분명히 버그-오류의 조건을 식별했을 수도 있지만 (nobugz가 나 때문에 그것을 발견했다고 말하지는 않습니다!), 이것 (그리고 당신, Nick도 +1이므로)은 JIT를 보여줍니다 범인입니다. IntVec이 클래스로 선언 될 때 최적화가 제거되거나 다르다는 점이 흥미 롭습니다. 루프 전에 구조체 필드를 0으로 명시 적으로 초기화하더라도 동일한 동작이 나타납니다. 추잡한!
Andras Zoltan

3
@Hans Passant 어셈블리 코드를 출력하기 위해 어떤 도구를 사용 했습니까?

3
@Joan-Visual Studio에서 디버거의 디스 어셈블리 창에서 복사 / 붙여 넣기 및 직접 추가 한 주석.
Hans Passant

82

나는 이것이 진정한 JIT 컴파일 버그에 있다고 생각합니다. 나는 그것을 Microsoft에보고하고 그들이 말하는 것을 볼 것입니다. 흥미롭게도 x64 JIT에는 동일한 문제가 없다는 것을 알았습니다.

다음은 x86 JIT에 대한 내용입니다.

// save context
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  

// put oDoesSomething pointer in ebx
00000006  mov         ebx,ecx 

// zero out edi, this will store oVec.y
00000008  xor         edi,edi 

// zero out esi, this will store oVec.x
0000000a  xor         esi,esi 

// NOTE: the inner loop is unrolled here.
// set oVec.y to 2
0000000c  mov         edi,2 

// call oDoesSomething.Do(oVec) -- y is always 2!?!
00000011  push        edi  
00000012  push        esi  
00000013  mov         ecx,ebx 
00000015  call        dword ptr ds:[002F0010h] 

// call oDoesSomething.Do(oVec) -- y is always 2?!?!
0000001b  push        edi  
0000001c  push        esi  
0000001d  mov         ecx,ebx 
0000001f  call        dword ptr ds:[002F0010h] 

// increment oVec.x
00000025  inc         esi  

// loop back to 0000000C if oVec.x < 2
00000026  cmp         esi,2 
00000029  jl          0000000C 

// restore context and return
0000002b  pop         ebx  
0000002c  pop         esi  
0000002d  pop         edi  
0000002e  pop         ebp  
0000002f  ret     

이것은 나에게 좋지 않은 최적화처럼 보입니다 ...


23

코드를 새 콘솔 앱에 복사했습니다.

  • 디버그 빌드
    • 디버거와 디버거가없는 올바른 출력
  • 릴리스 빌드로 전환
    • 다시, 두 번 올바른 출력
  • 새로운 x86 구성 생성 (X64 Windows 2008을 실행 중이며 'Any CPU'를 사용 중임)
  • 디버그 빌드
    • F5와 CTRL + F5 모두 올바른 출력을 얻었습니다.
  • 릴리스 빌드
    • 디버거가 연결된 올바른 출력
    • 디버거 없음- 잘못된 출력을 얻음

따라서 코드를 잘못 생성하는 x86 JIT입니다. 루프 재 배열 등에 관한 원본 텍스트를 삭제했습니다. 여기에 대한 다른 답변은 x86에서 JIT가 루프를 잘못 풀고 있음을 확인했습니다.

문제를 해결하기 위해 IntVec의 선언을 클래스로 변경할 수 있으며 모든 취향에서 작동합니다.

이것이 MS Connect에서 진행되어야한다고 생각하십시오 ....

Microsoft에 -1!


1
흥미로운 아이디어이지만, 이것이 "최적화"가 아니라 컴파일러의 경우 매우 중대한 버그입니까? 지금까지 발견되지 않았을까요?
David M

동의합니다. 이와 같이 루프를 재정렬하면 전례없는 문제가 발생할 수 있습니다. for 루프가 2에 도달 할 수 없기 때문에 실제로 이것은 훨씬 덜 보인다.
Andras Zoltan

2
이 더러운 Heisenbugs 중 하나처럼 보입니다 : P
arul

OP (또는 자신의 응용 프로그램을 사용하는 사람)에게 32 비트 x86 컴퓨터가 있으면 CPU가 작동하지 않습니다. 문제는 최적화가 활성화 된 x86 JIT가 잘못된 코드를 생성한다는 것입니다.
Nick Guerrera
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.