С# и С++ - почему .NET не выполняет самые основные оптимизации (например, удаление мертвого кода)?

Я серьезно сомневаюсь в том, что компиляторы С# или .NET JIT выполняют любую полезную оптимизацию, а тем более, если они действительно конкурентоспособны с самыми базовыми в компиляторах на С++.

Рассмотрим эту чрезвычайно простую программу, которую мне удобно сделать действительной как на С++, так и на С#:

#if __cplusplus
#else
static class Program
{
#endif
    static void Rem()
    {
        for (int i = 0; i < 1 << 30; i++) ;
    }
#if __cplusplus
    int main()
#else
    static void Main()
#endif
    {
        for (int i = 0; i < 1 << 30; i++)
            Rem();
    }
#if __cplusplus
#else
}
#endif

Когда я компилирую и запускаю его в новейшей версии С# (VS 2013) в режиме выпуска, он не завершается в течение разумного промежутка времени.

Изменить: Здесь еще один пример:

static class Program
{
    private static void Test2() { }

    private static void Test1()
    {
#if TEST
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
        Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
#else
        Test2();
#endif
    }

    static void Main()
    {
        for (int i = 0; i < 0x7FFFFFFF; i++)
            Test1();
    }
}

Когда я запускаю этот, он занимает лот дольше, если TEST определен, хотя все это не-op, а Test2 должен быть встроен.

Даже самые древние компиляторы С++, которые я могу взять в руки, однако, оптимизируют все, делая программы немедленно.

Что не позволяет оптимизатору .NET JIT быть в состоянии сделать такие простые оптимизации? Почему?

Ответы

Ответ 1

.NET JIT - плохой компилятор, это правда. К счастью, новый JIT (RyuJIT) и NGEN, который, кажется, основан на компиляторе VC, находятся в работе (я считаю, что это что использует Компилятор облаков Windows Phone.

Хотя это очень простой компилятор, он делает встроенные небольшие функции и в определенной степени удаляет свободные контуры побочных эффектов. Это не очень хорошо, но это происходит.

Прежде чем перейти к подробным выводам, обратите внимание, что x86 и x64 JIT - это разные кодовые базы, работают по-разному и имеют разные ошибки.


Тест 1:

Вы запускали программу в режиме Release в 32-битном режиме. Я могу воспроизвести ваши результаты на .NET 4.5 с 32-разрядным режимом. Да, это неловко.

В 64-битном режиме, однако, Rem в первом примере встроен и внутренняя из двух вложенных циклов удаляется:

enter image description here

Я отметил три инструкции цикла. Внешний контур все еще существует. Я не думаю, что это когда-либо имеет значение на практике, потому что у вас редко есть две вложенные мертвые петли.

Обратите внимание, что цикл был развернут 4 раза, затем развернутые итерации были свернуты в одну итерацию (разворачивание создало i += 1; i+= 1; i+= 1; i+= 1; и было свернуто до i += 4;). Разумеется, весь цикл можно было бы оптимизировать, но JIT действительно выполнял наиболее важные на практике вещи: разворачивание циклов и упрощение кода.

Я также добавил следующее в Main, чтобы упростить отладку:

    Console.WriteLine(IntPtr.Size); //verify bitness
    Debugger.Break(); //attach debugger


Тест 2:

Я не могу полностью воспроизвести ваши результаты в 32-битном или 64-битном режиме. Во всех случаях Test2 встроен в Test1, что делает его очень простой функцией:

enter image description here

Main вызывает Test1 в цикле, потому что Test1 слишком велик для встроенного (потому что не упрощенный размер подсчитывается, потому что методы JIT'ы изолированы).

Если у вас есть только один вызов Test2 в Test1, то обе функции достаточно малы, чтобы быть встроенными. Это позволяет JIT для Main обнаруживать, что в этом коде ничего не делается вообще.


Окончательный ответ: Надеюсь, я смогу пролить свет на то, что происходит. В этом процессе я обнаружил некоторые важные оптимизации. JIT просто не очень тщательна и полна. Если бы одна и та же оптимизация была просто выполнена во втором идентификационном проходе, здесь было бы намного упрощено. Но большинству программ нужен только один проход через все упрощения. Я согласен с выбором команды JIT, сделанной здесь.

Так почему же JIT так плохо? Одна часть состоит в том, что он должен быть быстрым, потому что JITing чувствителен к задержкам. Другая часть состоит в том, что это всего лишь примитивный JIT и требует больших инвестиций.