Потенциальная ошибка .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 с включенной оптимизацией генерирует плохой код.
Ник Геррера