Потенциальная ошибка .NET JIT?


404

Следующий код дает различный вывод при запуске выпуска внутри Visual Studio и при запуске выпуска вне Visual Studio. Я использую Visual Studio 2008 и ориентируюсь на .NET 3.5. Я также пробовал .NET 3.5 SP1.

При работе вне Visual Studio должен включиться JIT. Либо (а) что-то неуловимое происходит с C #, которого мне не хватает, либо (б) 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 - поздравляю!
Андрас Золтан

73
Похоже, это было воспроизведено в моей сборке 4.0 на платформе 4.0 на платформе x86 от 9 декабря. Я передам это команде джиттера. Спасибо!
Эрик Липперт

28
Это один из немногих вопросов, которые действительно заслуживают золотой значок.
Мехрдад Афшари

28
Тот факт, что мы все заинтересованы в этом вопросе, показывает, что мы не ожидаем ошибок в .NET JIT, хорошо сделанная Microsoft.
Ян Рингроз

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 года, эта ошибка была исправлена ​​в версии 4.0.30319 джиттер. Но все еще присутствует в джиттер v2.0.50727. Кажется маловероятным, что они исправят это в старой версии после этого долгого времени.


3
+1, определенно ошибка - я мог бы определить условия для ошибки (не говоря уже о том, что nobugz нашел ее из-за меня!), Но это (и ваш, Ник, так что +1 для вас тоже) показывает, что JIT виновник Интересно, что оптимизация либо удаляется, либо отличается, когда IntVec объявлен как класс. Даже если вы явно инициализируете структурные поля в 0 перед циклом, вы увидите такое же поведение. Противный!
Андрас Золтан

3
@Hans Passant Какой инструмент вы использовали для вывода кода сборки?

3
@Joan - Просто Visual Studio, скопируйте / вставьте из окна разборки отладчика и добавьте комментарии вручную.
Ганс Пассант

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

Я скопировал ваш код в новое консольное приложение.

  • Debug Build
    • Правильный вывод как с отладчиком, так и без отладчика
  • Переключился на Release Build
    • Опять же, правильный вывод оба раза
  • Создана новая конфигурация x86 (я работаю на X64 Windows 2008 и использую 'Any CPU')
  • Debug Build
    • Получил правильный вывод и F5 и CTRL + F5
  • Release Build
    • Правильный вывод с отладчиком
    • Нет отладчика - получен неверный вывод

Так что это x86 JIT неправильно генерирует код. Удалили мой оригинальный текст о переупорядочении циклов и т. Д. Несколько других ответов подтвердили, что JIT неправильно разматывает цикл в x86.

Чтобы решить эту проблему, вы можете изменить объявление IntVec на класс, и он работает во всех вариантах.

Думаю, для этого нужно перейти на MS Connect ....

-1 в Microsoft!


1
Интересная идея, но, конечно, это не «оптимизация», а очень серьезная ошибка в компиляторе, если это так? Был бы найден сейчас, не так ли?
Дэвид М

Я с тобой согласен. Такое переупорядочение циклов может вызвать неисчислимые проблемы. На самом деле это кажется еще менее вероятным, потому что петли for не могут когда-либо достичь 2.
Андрас Золтан

2
Похоже на одного из этих отвратительных гейзенбагов: P
arul

Любой процессор не будет работать, если OP (или кто-либо, использующий его приложение) имеет 32-битный компьютер x86. Проблема в том, что x86 JIT с включенной оптимизацией генерирует плохой код.
Ник Геррера
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.