为什么开放类型谓词<;T>;比它的对手慢

本文关键字:对手 gt 谓词 类型 lt 为什么 | 更新日期: 2023-09-27 18:26:10

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

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

谓词需要150毫秒,而普通比较需要125毫秒。从哪里来

差异/开销?你可能会问我为什么喜欢25毫秒。嗯。。。我在层次查找方法中也使用了开放类型谓词,差距要大得多。

所以我想lambda(为每个"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);
            }

为什么开放类型谓词<;T>;比它的对手慢

我认为速度上有一个小差异,但我的第一点是,在您的测试中,我需要改进一些地方。在构建这些微基准测试时,始终遵守以下几条规则是很重要的:

  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

不过,总的来说,我希望在通过本机调用调用delegate/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%的时间进行这种调用(例如),那么在执行代码时,这相当于一个绝对微小的差异。

我希望委托的速度会慢一些。在所有的委托参数都应该加载到寄存器或其他地方,然后应该执行一个方法调用,这是一个又一个跳转指令。你为什么期望他们是一样的?