Оптимизация JIT & loop

using System; 

namespace ConsoleApplication1
{ 
    class TestMath
    {  
        static void Main()
        {
            double res = 0.0;

            for(int i =0;i<1000000;++i)
                res +=  System.Math.Sqrt(2.0);

            Console.WriteLine(res);

            Console.ReadKey();  
        }
    }
}

Сравнивая этот код с версией на С++, я обнаружил, что производительность в 10 раз медленнее, чем версия С++. У меня нет проблем с этим, но это привело меня к следующему вопросу:

Кажется (после нескольких поисков), что JIT-компилятор не может оптимизировать этот код, как может сделать компилятор С++, а именно просто вызвать sqrt один раз и применить к нему * 1000000.

Есть ли способ заставить JIT сделать это?

Ответы

Ответ 1

Я воспроизвожу, я запускаю версию С++ на 1.2 мс, версию С# на 12.2 мс. Причина очевидна, если взглянуть на машинный код, который генерирует генератор кода и оптимизатор кода С++. Он переписывает такой цикл (используя эквивалент С#):

double temp = Math.Sqrt(2.0);
for (int i = 0; i < 1000000; ++i) {
    res += temp;
}

Это комбинация двух оптимизаций, называемых "инвариантным движением кода" и "подъемом петли". Другими словами, компилятор С++ достаточно знает о функции sqrt(), чтобы знать, что окружающее его значение не влияет на его возвращаемое значение, поэтому его можно перемещать по желанию. И тогда стоит переместить этот код за пределы цикла и создать дополнительную локальную переменную для хранения результата. И вычисление sqrt() происходит медленнее, чем добавление. Звучит очевидно, но это правило, которое должно быть встроено в оптимизатор и должно рассматриваться как одно из многих правил.

И да, оптимизатор дрожания пропустил этот. Он виноват в том, что не может потратить столько же времени, сколько оптимизатор на С++, он работает в тяжелых временных ограничениях. Потому что, если это занимает слишком много времени, программа начинает слишком много времени.

Язык в щеке: программист на С# должен быть немного умнее генератора кода и сам распознавать эти возможности оптимизации. Это довольно очевидно. Ну, теперь, когда вы все равно знаете об этом:)

Ответ 2

Чтобы сделать нужную оптимизацию, компилятор должен убедиться, что функция Sqrt() всегда будет возвращать одно и то же значение для определенного ввода.

Компилятор может выполнять все виды проверок, чтобы функция не использовала другие "внешние" переменные, чтобы увидеть, не является ли она безстоящей. Но это также не всегда означает, что на него не могут влиять побочные эффекты.

Когда функция вызывается в цикле, ее следует вызывать на каждой итерации (подумайте о многопоточной среде, чтобы понять, почему это важно). Поэтому обычно для пользователя требуется постоянный поток из цикла, если он хочет такую ​​оптимизацию.

Вернемся к компилятору С++ - у компилятора может быть определенная оптимизация для его библиотечных функций. Многие компиляторы пытаются оптимизировать важные библиотеки, такие как математическая библиотека, поэтому могут быть специфическими для компилятора.

Другое большое различие заключается в том, что в С++ вы обычно включаете этот материал из файла заголовка. Это означает, что компилятор может иметь всю необходимую ему информацию, чтобы решить, не вызывает ли вызов функции между вызовами.

Компилятор .Net(во время компиляции - Visual Studio) не всегда имеет весь синтаксический анализ. Большинство библиотечных функций уже скомпилированы (на ИЛ - первый этап). И, возможно, не удастся сделать глубокую оптимизацию с учетом сторонних DLL. И в компиляции JIT (runtime), вероятно, будет слишком дорогостоящим, чтобы делать такие оптимизации в сборках.

Ответ 3

Это может помочь JIT (или даже компилятору С#), если Math.Sqrt был аннотирован как [Pure]. Затем, если аргументы функции постоянны, как они есть в вашем примере, вычисление значения может быть снято вне цикла.

Что еще, такой цикл можно разумно преобразовать в код:

double res = 1000000 * Math.Sqrt(2.0);

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

Я открыл запрос функции для ReSharper, предполагая, что инструмент времени разработки предлагает такой рефакторинг.