Эффективность стека делегатов - PullRequest
7 голосов
/ 26 апреля 2011

Предположим, я написал такой класс (количество функций на самом деле не имеет значения, но в действительности их будет где-то около 3 или 4).

    private class ReallyWeird
    {
        int y;

        Func<double, double> f1;
        Func<double, double> f2;
        Func<double, double> f3;

        public ReallyWeird()
        {
            this.y = 10;

            this.f1 = (x => 25 * x + y);
            this.f2 = (x => f1(x) + y * f1(x));
            this.f3 = (x => Math.Log(f2(x) + f1(x)));
        }

        public double CalculusMaster(double x)
        {
            return f3(x) + f2(x);
        }
    }

Интересно, может ли компилятор C # оптимизировать такой код, чтобы он не проходил через многочисленные вызовы стека?

Можно ли вообще встроить делегатов во время компиляции? Если да, на каких условиях и в каких пределах? Если нет, есть ли ответ, почему?

Другой вопрос, может быть, даже более важный: будет ли он значительно медленнее, чем если бы я объявил f1, f2 and f3 как методы?

Я спрашиваю об этом, потому что хочу сохранить мой код максимально сухим, поэтому я хочу реализовать статический класс, который расширяет функциональность базового генератора случайных чисел (RNG): его методы принимают один делегат (например, из метода NextInt()). RNG) и возвращение другого Func делегата (например, для генерации ulong s), построенного поверх первого. и до тех пор, пока существует множество различных ГСЧ, которые могут генерировать int с, я предпочитаю не думать о реализации одной и той же расширенной функциональности десять раз в разных местах.

Таким образом, эта операция может выполняться несколько раз (т. Е. Начальный метод класса может быть «обернут» делегатом дважды или даже три раза). Интересно, что будет с производительностью?

Спасибо!

Ответы [ 3 ]

2 голосов
/ 26 апреля 2011

Я бы не ожидал, что компилятор оптимизирует это. Осложнения (из-за делегатов) были бы огромными.

И я бы не стал беспокоиться о нескольких фреймах стека. С 25 * x + y издержки на стек + вызов могут быть значительными, но вызовите несколько других методов (PRNG), и часть, на которой вы здесь сосредоточены, становится очень незначительной.

2 голосов
/ 26 апреля 2011

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

При выполнении 10.000.000 вычислений для каждой версии я получил следующие результаты:

  • Работа с использованием делегатов: в среднем 920 мс
  • Работа с использованием обычных вызовов методов: в среднем 730 мс

Так что, несмотря на разницу, она не очень большая ивероятно, незначительный.

Теперь в моих вычислениях может быть ошибка, поэтому я добавляю весь код ниже.Я скомпилировал его в режиме выпуска в Visual Studio 2010:

class Program
{
    const int num = 10000000;

    static void Main(string[] args)
    {

        for (int run = 1; run <= 5; run++)
        {
            Console.WriteLine("Run " + run);
            RunTest1();
            RunTest2();
        }
        Console.ReadLine();
    }

    static void RunTest1()
    {

        Console.WriteLine("Test1");

        var t = new Test1();

        var sw = Stopwatch.StartNew();
        double x = 0;
        for (var i = 0; i < num; i++)
        {
            t.CalculusMaster(x);
            x += 1.0;
        }
        sw.Stop();

        Console.WriteLine("Total time for " + num + " iterations: " + sw.ElapsedMilliseconds + " ms");

    }

    static void RunTest2()
    {

        Console.WriteLine("Test2");

        var t = new Test2();

        var sw = Stopwatch.StartNew();
        double x = 0;
        for (var i = 0; i < num; i++)
        {
            t.CalculusMaster(x);
            x += 1.0;
        }
        sw.Stop();

        Console.WriteLine("Total time for " + num + " iterations: " + sw.ElapsedMilliseconds + " ms");

    }
}

class Test1 
{
    int y;

    Func<double, double> f1;
    Func<double, double> f2;
    Func<double, double> f3;

    public Test1()
    {
        this.y = 10;

        this.f1 = (x => 25 * x + y);
        this.f2 = (x => f1(x) + y * f1(x));
        this.f3 = (x => Math.Log(f2(x) + f1(x)));
    }

    public double CalculusMaster(double x)
    {
        return f3(x) + f2(x);
    }

}
class Test2
{
    int y;


    public Test2()
    {
        this.y = 10;
    }

    private double f1(double x)
    {
        return 25 * x + y;
    }

    private double f2(double x)
    {
        return f1(x) + y * f1(x);
    }

    private double f3(double x)
    {
        return Math.Log(f2(x) + f1(x));
    }

    public double CalculusMaster(double x)
    {
        return f3(x) + f2(x);
    }

}
2 голосов
/ 26 апреля 2011

Если вы используете Деревья выражений вместо полного Func <>, компилятор сможет оптимизировать выражения.

Редактировать Чтобы уточнить, обратите внимание, что я не говорю, что среда выполнения оптимизирует само дерево выражений (а не должно), а скорее потому, что результирующее дерево Expression<> равно .Compile() d в на одном шаге механизм JIT просто увидит повторяющиеся подвыражения и сможет оптимизировать, объединить, заменить, создать ярлык и все, что он обычно делает.

(я не совсем уверен, что он работает на всех платформах, но, по крайней мере, он должен полностью использовать механизм JIT)


Комментарий ответа

  • Во-первых, у деревьев выражений в потенциале должна быть скорость выполнения, равная Func <> (однако Func <> не будет иметь таких же затрат времени выполнения - JITting, вероятно, имеет место при джитировании охватывающей области; в случае ngen, это даже будет AOT, в отличие от дерева выражений)

  • Второе: я согласен с тем, что деревья выражений могут быть сложными в использовании. Смотрите здесь известный простой пример того, как составлять выражения. Однако найти более сложные примеры довольно сложно. Если у меня будет время, я посмотрю, смогу ли я придумать PoC и посмотреть, что MS.Net и MONO фактически генерируют в MSIL для этих случаев.

  • Третье: не забывайте Хенк Холтерман , вероятно, прав, говоря, что это преждевременная оптимизация (хотя составление Expression<> вместо Func<> раньше времени добавляет гибкости)

  • Наконец, когда вы действительно задумываетесь о продвижении так далеко, вы можете подумать об использовании Compiler As A Service (который уже есть в Mono, я полагаю, он все еще готов для Microsoft?).

...