LINQ to Objects速度慢。现在我只是完全糊涂了

时间:2010-10-22 19:16:15

标签: c# linq generics

好的,基于我之前的问题: Generically checking for null that won't box nullables on a non-constrained type.

一个用户建议为类和struct添加约束,我还实现了专门针对三种类型的UnboxT模式,并将该逻辑存储在委托中。我还被告知尝试使用OfType<T>

以下是我的方法:

public static class Check<T>
{
    public static readonly Func<T,bool> IfNull = CreateIfNullDelegate();
    private static bool AlwaysFalse(T obj)
    {
        return false;
    }

    private static bool ForRefType(T obj)
    {
        return object.ReferenceEquals(obj, null);
    }

    private static bool ForNullable<Tu>(Tu? obj) where Tu:struct
    {
        return !obj.HasValue;
    }

    private static Func<T,bool> CreateIfNullDelegate()
    {
        if (!typeof(T).IsValueType)
            return ForRefType;
        else
        {
            Type underlying;
            if ((underlying = Nullable.GetUnderlyingType(typeof(T))) != null)
            {
                return Delegate.CreateDelegate(
                    typeof(Func<T,bool>),
                    typeof(Check<T>)
                     .GetMethod("ForNullable",BindingFlags.NonPublic | BindingFlags.Static)
                      .MakeGenericMethod(underlying)
                ) as Func<T,bool>;
            }
            else
            {
                return AlwaysFalse;
            }
        }
    }
}
public static int CountNonNull<T>(this IEnumerable<T> enumerable) where T:class
{
    return enumerable.Count(x=>Object.ReferenceEquals(x,null));
}

public static int CountNonNull<T>(this IEnumerable<T?> enumerable) where T : struct
{
    return enumerable.Count(x=>x!=null);
}

public static int CountNonNull3<T>(this IEnumerable<T> enumerable)
{
    return enumerable.OfType<T>().Count();
}

public static int CountNonNull2<T>(this IEnumerable<T> enumerable)
{
    return enumerable.Count(Check<T>.IfNull);
}
public static void Profile(this Action action, string name, int times = 1 * 1000 * 1000, bool display = true)
{
    for (int i = 0; i < 100; i++)
    {
        action();
    }
    var _stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < times; i++)
    {
        action();
    }
    _stopwatch.Stop();
    if (display)
    {
        Console.WriteLine("{0}: did {1} iterations in {2} ms", name, times, _stopwatch.ElapsedMilliseconds);
    }
}

以下是我的测试集:

var ints = Enumerable.Range(0,10).ToArray();
var nullableInts = Array.ConvertAll(ints,x=>x as int?);
var strings = Array.ConvertAll(ints,x => x.ToString());
Profile(() => nullableInts.CountNonNull(), "int?");
Profile(() => strings.CountNonNull(), "strings");
Profile(() => nullableInts.CountNonNull2(), "int? 2");
Profile(() => strings.CountNonNull2(), "strings 2");
Profile(() => nullableInts.CountNonNull3(), "int? 3");
Profile(() => strings.CountNonNull3(), "strings 3");

以下是我的结果:

int?: did 1000000 iterations in 188 ms
strings: did 1000000 iterations in 2346 ms
int? 2: did 1000000 iterations in 180 ms
strings 2: did 1000000 iterations in 147 ms
int? 3: did 1000000 iterations in 4120 ms
strings 3: did 1000000 iterations in 859 ms

OfType<T>缓慢有必要做is然后进行演员表演。这意味着它必须通过集合执行两个循环来确定其结果(尽管int?时间很难相信)。

但是第一种和第二种方法都执行相同的谓词。为什么第一个在字符串上表现得这么慢,而第二个表现得像一个冠军?

编辑:因为额外的疯狂: 在试验中添加另一种方法:

public static int CountNonNull4(this System.Collections.IEnumerable enumerable)
{
    return enumerable.Cast<object>().Count(x => object.ReferenceEquals(x, null));
}

此版本产生:

strings 4: did 1000000 iterations in 677 ms

这几乎没有意义。发生了什么事?

2 个答案:

答案 0 :(得分:0)

您意识到StopWatch不考虑实际的线程活动,对吧?这相当于你早上通勤上班的时间;有很多事情可能会阻碍你的进步,每天都会有不同的数量(有一天你会抓住一盏灯,阻止你下一次,交通拥堵等等)。

这个比喻在计算机中非常好;操作系统可能已经中断了您的线程以执行其他操作,您的线程可能必须等待页面文件操作(扩展,交换)等。尝试运行每个算法2或3次并平均时间。此外,请确保您的应用程序在FullTrust中运行,它绕过所有安全性(但不是运行时完整性)权限检查。最后,如果你能以某种方式多线程化这个分析器,你可以获得有关算法所需的实际循环次数的指标,这将与线程调度延迟无关。

答案 1 :(得分:0)

这是对enumerable.Count的调用。当我将数组的大小增加1000并将性能测试的迭代减少1000时,它们执行的几乎相同,我得到以下结果:

int?: did 1000 iterations in 488 ms
strings: did 1000 iterations in 437 ms

通过此测试,字符串版本实际上更快。

原因似乎是,对于struct版本,编译器可以内联对enumerable.Count的调用。但是,对于class版本,它会在下面生成IL。解释在代码中。

.method public hidebysig static int32  CountNonNull<class T>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!T> enumerable) cil managed
{
    .custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) 
    .maxstack  4
    .locals init ([0] int32 CS$1$0000)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldnull
    // Push the lambda for ReferenceEquals onto the stack.
    IL_0003:  ldftn      bool ConsoleApplication7.Program::'<CountNonNull>b__8'<!!0>(!!0)
    // Create a new delegate for the lambda.
    IL_0009:  newobj     instance void class [mscorlib]System.Func`2<!!T,bool>::.ctor(object,
                                                                                    native int)
    // Call the Count Linq method.
    IL_000e:  call       int32 [System.Core]System.Linq.Enumerable::Count<!!0>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,
                                                                                class [mscorlib]System.Func`2<!!0,bool>)
    IL_0013:  stloc.0
    IL_0014:  br.s       IL_0016
    IL_0016:  ldloc.0
    IL_0017:  ret
}

对于struct版本,它不必执行任何操作,它只是内联这样的内容:

var enumerator = enumerable.GetEnumerator();
int result = 0;

try
{
    while (true)
    {
        var current = enumerator.Current;

        if (current.HasValue)
            result++;

        if (!enumerator.MoveNext())
            break;
    }
}
finally
{
    enumerator.Dispose();
}

这当然比class版本的IL快得多。