Лямбды внутри generic методов в 200 раз медленнее
От: Alexander Polyakov  
Дата: 05.12.11 14:25
Оценка: 76 (10)
Это можно увидеть вот на таком тесте, см. код ниже. Результаты:
direct call 8ms
generic 1881ms
fixed generic 29ms
reflection 414ms

Я с этим столкнулся еще в 2008 году, надеялся, что пофиксят, а оно до сих пор .

Вот тут, похоже, тоже об этом пишут:

Creating a delegate pointing to generic virtual method time is 1000x.
http://tips.x-tensive.com/2008/10/method-call-performance.html



using System;
using System.Diagnostics;
using System.Reflection;

namespace ConsoleApplication9
{
    class Program
    {
        static void Main(   )
        {
            const int iterations = 500000;

            Method2<string>();
            Method2<string>();

            Method3<string>();
            Method3<string>();

            Method1();
            Method1();

            var methodInfo = typeof(Program).GetMethod("Method1");

            methodInfo.Invoke(null, new object[] { });
            methodInfo.Invoke(null, new object[] { });

            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (int i = 0; i < iterations; i++)
                {
                    Method1();
                    Method1();
                }
                stopwatch.Stop();
                Console.WriteLine("direct call " + stopwatch.ElapsedMilliseconds + "ms");
            }

            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (int i = 0; i < iterations; i++)
                {
                    Method2<string>();
                    Method2<string>();
                }
                stopwatch.Stop();
                Console.WriteLine("generic " + stopwatch.ElapsedMilliseconds + "ms");
            }

            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (int i = 0; i < iterations; i++)
                {
                    Method3<string>();
                    Method3<string>();
                }
                stopwatch.Stop();
                Console.WriteLine("fixed generic " + stopwatch.ElapsedMilliseconds + "ms");
            }

            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (int i = 0; i < iterations; i++)
                {
                    methodInfo.Invoke(null, new object[] { });
                    methodInfo.Invoke(null, new object[] { });
                }
                stopwatch.Stop();
                Console.WriteLine("reflection " + stopwatch.ElapsedMilliseconds + "ms");
            }
        }

        public static bool Method1()
        {
            Func<string> func1 = () => string.Empty;
            Func<Type> func2 = () => null;
            func1();
            func2();
            return true;
        }

        public static bool Method2<T>()
        {
            Func<string> func1 = () => string.Empty;
            Func<Type> func2 = () => null;
            func1();
            func2();
            return true;
        }

        public static bool Method3<T>()
        {
            return Class1<T>.Method2();
        }

        private static class Class1<T>
        {
            public static bool Method2()
            {
                Func<string> func1 = () => string.Empty;
                Func<Type> func2 = () => null;
                func1();
                func2();
                return true;
            }
        }
    }
}
Re: Лямбды внутри generic методов в 200 раз медленнее
От: Пельмешко Россия blog
Дата: 05.12.11 15:07
Оценка:
Здравствуйте, Alexander Polyakov, Вы писали:

AP>Это можно увидеть вот на таком тесте, см. код ниже. Результаты:

AP>direct call 8ms
AP>generic 1881ms
AP>fixed generic 29ms
AP>reflection 414ms

AP>Я с этим столкнулся еще в 2008 году, надеялся, что пофиксят, а оно до сих пор .


AP>Вот тут, похоже, тоже об этом пишут:


AP>

AP>Creating a delegate pointing to generic virtual method time is 1000x.
AP>http://tips.x-tensive.com/2008/10/method-call-performance.html


Проблема создание делегатов из generic-методов известна и непонятно, пофиксят ли её и возможно ли чтобы пофиксили вообще, может какая-то специфика reified generics мешает. Если создание делегата из method-group в критичном к производительности коде ещё можно легко обходить стороной, оборачивая в тривиальную лямбду вида x => Method(x), то Ваш пример показал ещё более проблемный случай — методы, генерируемые для представления лямбда-выражений внутри generic-метода сами являются generic-методами (чтобы внутри лямбд был доступ к T) и создание делегатов из них начинает тормозить... Тут уже сколько не оборачивай в лямбды, все они будут generic-методами, не смотря на отсутствие использования в них T.

Это всё печально
Re[2]: Лямбды внутри generic методов в 200 раз медленнее
От: Alexander Polyakov  
Дата: 05.12.11 15:58
Оценка:
Здравствуйте, Пельмешко, Вы писали:

П>методы, генерируемые для представления лямбда-выражений внутри generic-метода сами являются generic-методами (чтобы внутри лямбд был доступ к T)

А что если навешивать T не на сгенерированный метод, а на сгенерированный класс. Т.е. это будет отдаленно напоминать реализацию замыканий для обычных переменных; там переменные замыкания становятся филдами генерируемого класса, а в нашем случае T будет уходить в generic параметр класса.
Re[3]: Лямбды внутри generic методов в 200 раз медленнее
От: Пельмешко Россия blog
Дата: 05.12.11 16:15
Оценка:
Здравствуйте, Alexander Polyakov, Вы писали:

AP>Здравствуйте, Пельмешко, Вы писали:


П>>методы, генерируемые для представления лямбда-выражений внутри generic-метода сами являются generic-методами (чтобы внутри лямбд был доступ к T)


AP>А что если навешивать T не на сгенерированный метод, а на сгенерированный класс. Т.е. это будет отдаленно напоминать реализацию замыканий для обычных переменных; там переменные замыкания становятся филдами генерируемого класса, а в нашем случае T будет уходить в generic параметр класса.


Ага, можно так и делать, получится Ваш третий пример, статические лямбды ещё и кэшироваться будут успешно.
Но обычно отдельные классы для лямбда-выражений, статических или замыкающихся только на this, не создаются вовсе и решать такую проблему на уровне компилятора как-то костыльно, пусть лучше рантайм починят уже.
Re: Лямбды внутри generic методов в 200 раз медленнее
От: VladD2 Российская Империя www.nemerle.org
Дата: 09.12.11 23:34
Оценка: 4 (1) +1
Здравствуйте, Alexander Polyakov, Вы писали:

AP>Это можно увидеть вот на таком тесте, см. код ниже. Результаты:

AP>direct call 8ms
AP>generic 1881ms
AP>fixed generic 29ms
AP>reflection 414ms

Повторил тест на Nemerle с использованием его функционального типа вместо дженериков. Результаты:
direct call 6 ms
generic 29 ms
fixed generic 30 ms
reflection 680 ms

То есть, проблем не наблюдается.

  Скрытый текст
using System;
using System.Diagnostics;
using System.Reflection;

namespace ConsoleApplication9
{
  module Program
  {
    Main(   ) : void
    {
      def iterations = 500000;

      _ = Method2.[string]();
      _ = Method2.[string]();

      _ = Method3.[string]();
      _ = Method3.[string]();

      _ = Method1();
      _ = Method1();

      def methodInfo = typeof(Program).GetMethod("Method1");

      _ = methodInfo.Invoke(null, array[]);
      _ = methodInfo.Invoke(null, array[]);

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = Method1();
          _ = Method1();
        }
        stopwatch.Stop();
        Console.WriteLine($"direct call $(stopwatch.ElapsedMilliseconds) ms");
      }

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = Method2.[string]();
          _ = Method2.[string]();
        }
        stopwatch.Stop();
        Console.WriteLine($"generic $(stopwatch.ElapsedMilliseconds) ms");
      }

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = Method3.[string]();
          _ = Method3.[string]();
        }
        stopwatch.Stop();
        Console.WriteLine($"fixed generic $(stopwatch.ElapsedMilliseconds) ms");
      }

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = methodInfo.Invoke(null, array[]);
          _ = methodInfo.Invoke(null, array[]);
        }
        stopwatch.Stop();
        Console.WriteLine($"reflection $(stopwatch.ElapsedMilliseconds) ms");
      }
    }

    public Method1() : bool
    {
      def func1 = () => string.Empty;
      def func2 = () => (null : Type);
      _ = func1();
      _ = func2();
      true
    }

    public Method2[T]() : bool
    {
      def func1 = () => string.Empty;
      def func2 = () => (null : Type);
      _ = func1();
      _ = func2();
      true
    }

    public Method3[T]() : bool
    {
      Class1.Method2();
    }

    public module Class1[T]
    {
      public Method2() : bool
      {
        def func1 = () => string.Empty;
        def func2 = () => (null : Type);
        _ = func1();
        _ = func2();
        true
      }
    }
  }
}


Далее повторил на делегатах. Тоже вроде ничего.
direct call 54 ms
generic 81 ms
fixed generic 99 ms
reflection 748 ms


  Скрытый текст
using System;
using System.Diagnostics;
using System.Reflection;

namespace ConsoleApplication9
{
  module Program
  {
    Main(   ) : void
    {
      def iterations = 500000;

      _ = Method2.[string]();
      _ = Method2.[string]();

      _ = Method3.[string]();
      _ = Method3.[string]();

      _ = Method1();
      _ = Method1();

      def methodInfo = typeof(Program).GetMethod("Method1");

      _ = methodInfo.Invoke(null, array[]);
      _ = methodInfo.Invoke(null, array[]);

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = Method1();
          _ = Method1();
        }
        stopwatch.Stop();
        Console.WriteLine($"direct call $(stopwatch.ElapsedMilliseconds) ms");
      }

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = Method2.[string]();
          _ = Method2.[string]();
        }
        stopwatch.Stop();
        Console.WriteLine($"generic $(stopwatch.ElapsedMilliseconds) ms");
      }

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = Method3.[string]();
          _ = Method3.[string]();
        }
        stopwatch.Stop();
        Console.WriteLine($"fixed generic $(stopwatch.ElapsedMilliseconds) ms");
      }

      {
        def stopwatch = Stopwatch();
        stopwatch.Start();
        for (mutable  i = 0; i < iterations; i++)
        {
          _ = methodInfo.Invoke(null, array[]);
          _ = methodInfo.Invoke(null, array[]);
        }
        stopwatch.Stop();
        Console.WriteLine($"reflection $(stopwatch.ElapsedMilliseconds) ms");
      }
    }

    public Method1() : bool
    {
      def func1 : Func[string] = () => string.Empty;
      def func2 : Func[Type] = () => null;
      _ = func1();
      _ = func2();
      true
    }

    public Method2[T]() : bool
    {
      def func1 : Func[string] = () => string.Empty;
      def func2 : Func[Type] = () => null;
      _ = func1();
      _ = func2();
      true
    }

    public Method3[T]() : bool
    {
      Class1.Method2();
    }
    
    module Class1[T]
    {
      public Method2() : bool
      {
        def func1 : Func[string] = () => string.Empty;
        def func2 : Func[Type] = () => null;
        _ = func1();
        _ = func2();
        true
      }
    }
  }
}


Попробовал задайствовать параметр типа в лямбде заменив:
def func2 : Func[Type] = () => null;

на
def func2 : Func[T] = () => default(T);


Результат:
direct call 55 ms
generic 84 ms
fixed generic 121 ms
reflection 735 ms


Так что явно в МС могли бы обойти проблему.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Лямбды внутри generic методов в 200 раз медленнее
От: Пельмешко Россия blog
Дата: 12.12.11 08:14
Оценка: 1 (1)
Здравствуйте, VladD2, Вы писали:

VD>Так что явно в МС могли бы обойти проблему.


Потому что шлёпаете класс на каждый функциональный тип:
private sealed class _N__N_lambda__7099__7112<T> : Function<string>
{
    public static readonly Program._N__N_lambda__7099__7112<T> Instance = new Program._N__N_lambda__7099__7112<T>();
    public sealed override string apply()
    {
        return string.Empty;
    }
}
вот generic-параметр и уезжает в этот тип, скрывая проблему рантайма

Очень клёво, что кэшируете значения функционального типа, а вот экземпляры делегатов не кэшируете. Не нужно?
Re[3]: Лямбды внутри generic методов в 200 раз медленнее
От: VladD2 Российская Империя www.nemerle.org
Дата: 12.12.11 14:24
Оценка:
Здравствуйте, Пельмешко, Вы писали:

П>Очень клёво, что кэшируете значения функционального типа, а вот экземпляры делегатов не кэшируете. Не нужно?


В N вообще кеширвоания меньше чем у компиляторах C#, но работает все как минимум не медленнее.

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

Если поглядеть тесты, что я привел, то там видно, что делегатная версия в 2-3 раза медленнее. Возможно из-за отсутствия кэширования, а возможно просто сами делегаты дают дополнительные накладные расходы. Но в целом это отличие не заметить в микроском. Ведь ФВП делают какую-то работу. И обычно она значительно более затратна чем накладные расходы на создание делегатов.

Потому, в Nemerle можно как жить без делегатов, жить без ФП как такового. Если код критичен к производительности, то просто не надо там использовать ФП. Такого кода обычно не много и его частенько можно сгенерировать по некому ДСЛ. Это позволяет делать немерловый код значительно более шустрам нежели его аналог на C#.

Ну, а раз есть возможность получить приемлемую абстракцию разными способами, то заниматься хардкорными оптимизациями не очень то и охота. Все равно в итоге они упираются в рантайм.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.