Почему предикат открытого типа <T>медленнее своего аналога - PullRequest
3 голосов
/ 29 февраля 2012

Почему Id == 999999 быстрее, чем сравнение, основанное на лямбда-выражениях, с использованием предиката?

Возможно, сам мой тест не на 100% такой же, но это всего лишь пример, показывающий что-то об общемВопрос: Является ли предикат медленнее, чем обычный Id == 999999?

Предикат занимает 150 мс, а обычное сравнение - 125 мс.Откуда берется разница / накладные расходы

?Вы можете спросить, почему я забочусь о 25 мс.Хорошо ... Я использую предикат открытого типа также в иерархическом методе поиска, и там разрыв намного больше.

Так что я думаю, что лямбда (создание для каждого "u" делегата) + предикат - это проблема?Если нет, то что не так с моей настройкой?

   public class UtilitiesTest
        {
            [Test]
            public void Go()
            {
                var units = GetUnits();
                DateTime d = DateTime.Now;         
                var item = units.testme<MyUnit>(u => u.Id == 999999);

                TimeSpan t = DateTime.Now - d;

                Debug.WriteLine(t.TotalMilliseconds + " ms");


                var units1 = GetUnits();
                DateTime d1 = DateTime.Now;  

                MyUnit item1 = null;
                foreach (MyUnit unit in units1)
                {
                    if (unit.Id == 999999)
                    {
                        item1 = unit;
                        break;
                    }
                }
                TimeSpan t1 = DateTime.Now - d1;
                Debug.WriteLine(t1.TotalMilliseconds + " ms");
            }

            private IEnumerable<MyUnit> GetUnits()
            {
                for (int i = 0; i < 1000000; i++)          
                    yield return new MyUnit() { Id = i };            
            }        
        }

        class MyUnit
        {
            public int Id { get; set; }
        }

    public static T testme<T>(this IEnumerable<T> source, Predicate<T> condition) where T : class
            {
                foreach (T item in source)
                {
                    if (condition(item))
                    {
                        return item;
                    }
                }
                return default(T);
            }

Ответы [ 2 ]

3 голосов
/ 29 февраля 2012

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

  1. При хронометраже используйте секундомер вместо DateTime - он имеет более высокое разрешение и точнее.
  2. Всегда убедитесь, что вы прогреваете весь тестируемый код один раз перед его запуском. В противном случае первый бит кода будет иметь тенденцию работать медленнее, поскольку он будет стоить большую часть JIT'ing
  3. Всегда запускайте тест несколько раз (вы сделали это в своем примере).

Если я переделываю ваш тест, я получаю что-то вроде этого:

public void Go()
{
    // warmup
    Test_Equality();
    Test_Lambda();

    // timed tests
    Console.WriteLine(Test_Equality() + " ms");
    Console.WriteLine(Test_Lambda() + " ms");
}

public long Test_Lambda()
{
    var units1 = GetUnits();
    var stopWatch1 = new Stopwatch();
    stopWatch1.Start();
    MyUnit item1 = units1.testme<MyUnit>(u => u.Id == 999999);
    return stopWatch1.ElapsedMilliseconds;
}

public long Test_Equality()
{
    var units2 = GetUnits();
    var stopWatch2 = new Stopwatch();
    stopWatch2.Start();
    MyUnit item2;
    foreach (MyUnit unit in units2)
    {
        if (unit.Id == 999999)
        {
            item2 = unit;
            break;
        }
    }

    return stopWatch2.ElapsedMilliseconds;
}

Я запускаю это, я получаю цифры, примерно такие:

Test_Lambda: 68 ms 
Test_Equality: 53 ms

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

В конечном итоге оно генерирует что-то в соответствии с this:

public class PossibleLambdaImpl
{
    public bool Comparison(MyUnit myUnit)
    {
        return myUnit.Id == 9999999;
    }
}

Следовательно, ваш лямбда-тест на самом деле вызывает метод для класса, сгенерированного компилятором, каждый раз, когда он оценивает.

Фактически - когда я изменяю ваш тест на равенство, чтобы вместо этого создать вышеупомянутый класс PossibleLambdaImpl один раз, и каждый раз вызывать PossibleLambdaImpl.Comparison каждый раз вокруг цикла, я получаю почти идентичные результаты для лямбда-случая:

public long Test_PossibleLambdaImpl()
{
    var units2 = GetUnits();
    var stopWatch2 = new Stopwatch();
    stopWatch2.Start();
    MyUnit item2;
    var possibleLambdaImpl = new PossibleLambdaImpl();
    foreach (MyUnit unit in units2)
    {
        if (possibleLambdaImpl.Comparison(unit))
        {
            item2 = unit;
            break;
        }
    }
    return stopWatch2.ElapsedMilliseconds;
}

[Примечание: на этом сайте есть другие, которые знают об этом гораздо больше, чем я, но, грубо говоря, я считаю, что это правильно]

В любом случае следует помнить, что эта разница в производительности ничтожна. Микро-тесты, подобные этому, всегда подчеркивают разницу. В зависимости от вашего теста между ними может быть разница в производительности на 10% -20%, но если ваш реальный код тратит только 0,001% своего времени на выполнение такого рода вызовов (например), то это равносильно крошечной разнице в выполнение кода.

0 голосов
/ 29 февраля 2012

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

...