为什么开放类型谓词<t>比对应</t>慢

时间:2012-02-29 08:40:45

标签: c# performance lambda predicate

为什么Id == 999999比使用谓词的lambda表达式驱动比较快?

也许我的测试本身并不是100%相同,但这只是一个展示一般问题的示例:谓词是否比普通Id = 999999慢?

谓词需要150 ms,而公共比较需要125 ms。

来自哪里

差/开销?你可能会问我为什么要关心25毫秒。嗯...我在分层查找方法中也使用了开放式谓词,差距要大得多。

所以我猜lambda(为每个“你”代表创建)+谓词是问题吗?如果没有我的设置有什么问题?

   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 个答案:

答案 0 :(得分:3)

我认为速度差异很小,但我的第一点是在测试中我会改进一些东西。在构建这些微基准测试时,始终遵守一些规则非常重要:

  1. 在计时时,使用秒表而不是DateTime - 它具有更高的分辨率,更精确。
  2. 在运行之前,请务必确保预热所有正在测试的代码。否则,第一部分代码将具有运行速度较慢的趋势,因为它承担了JIT的大部分成本
  3. 始终多次运行测试(您已在示例中完成此操作)。
  4. 如果我重做你的测试,我最终会得到这样的结果:

    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
    

    总的来说,我希望在本机调用上调用委托/ lambda版本时会有很小的性能损失,就像通过委托调用方法而不是调用方法直接。在幕后,编译器正在生成额外的代码来支持这些lambda版本的测试。

    最终它正在生成

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

    因此,lambda测试实际上是在每次评估时在编译器生成的类上调用一个方法。

    事实上 - 当我改变你的相等测试而不是创建上面的PossibleLambdaImpl类一次,并且每次循环调用PossibleLambdaImpl.Comparison时,我得到的结果几乎与lambda情况相同:

    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%的时间进行此类调用(例如),那么这相当于执行代码。

答案 1 :(得分:0)

我希望代表会变慢。在所有委托参数应该在寄存器或其他任何内容中加载之后,然后应该执行一个方法调用,这是一个多跳转指令。你为什么期望它们是一样的?