为什么linq在c#中很慢
Why is linq slow in c#
我突然对 linq 性能和 运行 一些测试感到好奇。
下面是我的测试代码,结果非常令人惊讶。
谁能知道 linq 是如何工作的,为什么比 TryOut 慢?
Public class TestObject
{
....
....
//this class contain many members
bool deleted;
....
}
class Program
{
public static ConcurrentDictionary<string, TestObject> testDictionary = new ConcurrentDictionary<string, TestObject>();
static void Main(string[] args)
{
//testDictionary is initialized in ohter code and is likely to have 10000 elements.
RandomTest(0);
RandomTest(1);
Console.ReadKey();
}
static void RandomTest(int k)
{
int count = 10000;
List<string> randomId = new List<string>();
Random rnd = new Random();
for (int i = 0; i < count; i++)
{
int randomNumber = rnd.Next(0, testDictionary.Count());
randomId.Add(testDictionary.ElementAt(randomNumber).key);
}
Stopwatch sw = new Stopwatch();
sw.Start();
if (k == 0)
{
for (int i = 0; i < count; i++)
{
var res = checkid(randomId[i]);
}
}
else if (k == 1)
{
for (int i = 0; i < count; i++)
{
var res = checkid2(randomId[i]);
}
}
sw.Stop();
Console.WriteLine("Elapsed time : " + sw.Elapsed);
}
static bool checkid(string id)
{
TestObject t;
return !testDictionary.TryGetValue(id, out t) ?
false : t.deleted ?
false : true;
}
static bool checkid2(string id)
{
return testDictionary.Any(t => t.key == id && !t.Value.deleted)? true : false;
}
我运行这两种方法10000次,结果如下
checkid方式,大部分用时不到00:00:00.002.
对于checkid2方法,大部分时间在00:00:02.2和00:00:02.4.
之间
这是一个巨大的差异。
这是因为checkid2方法即使key不等于Id也会检查删除的变量,而checkid方法只有在找到对应的key时才会检查删除的变量?
Dictionary.TryGetValue
使用散列来定位元素,因此它是一个 O(1) 操作。
Dictionary.Any
将遍历集合以尝试找到符合条件的集合。那是 O(n).
通常 - LINQ 会比使用 for
/foreach
的 hand-crafting 循环慢一点,但在大多数情况下性能差异无关紧要。您在这里遇到的不是 LINQ 变慢,而是 Dictionary<T>.TryGetValue
变快,因为它的内部结构针对 key-based 搜索进行了优化。如果您将其更改为 List<T>
并编写 for
循环以线性方式执行相同的搜索(就像 LINQ 在幕后所做的那样),您会发现差异变得更小。
@MarcinJuraszek 回答正确。但我想添加一些相关的 LINQ 信息来完成答案。是的,散列与迭代行为的主要区别很明显,但是您需要了解更多关于 .NET 中的 LINQ
对于 checkid
,IL 如下所示:
IL_0000: ldsfld class [mscorlib]System.Collections.Concurrent.ConcurrentDictionary`2<string, class ConsoleApp5.TestObject> ConsoleApp5.Program::testDictionary
IL_0005: ldarg.0
IL_0006: ldloca.s t
IL_0008: callvirt instance bool class [mscorlib]System.Collections.Concurrent.ConcurrentDictionary`2<string, class ConsoleApp5.TestObject>::TryGetValue(!0, !1&)
IL_000d: brfalse.s IL_001b
IL_000f: ldloc.0
IL_0010: ldfld bool ConsoleApp5.TestObject::deleted
IL_0015: brtrue.s IL_0019
IL_0017: ldc.i4.1
IL_0018: ret
IL_0019: ldc.i4.0
IL_001a: ret
IL_001b: ldc.i4.0
IL_001c: ret
它完全按照你的想法去做(你在代码中写的)。
但是 checkid2
这样做:
IL_0000: newobj instance void ConsoleApp5.Program/'<>c__DisplayClass4_0'::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld string ConsoleApp5.Program/'<>c__DisplayClass4_0'::id
IL_000d: ldsfld class [mscorlib]System.Collections.Concurrent.ConcurrentDictionary`2<string, class ConsoleApp5.TestObject> ConsoleApp5.Program::testDictionary
IL_0012: ldloc.0
IL_0013: ldftn instance bool ConsoleApp5.Program/'<>c__DisplayClass4_0'::'<checkid2>b__0'(valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>)
IL_0019: newobj instance void class [mscorlib]System.Func`2<valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>, bool>::.ctor(object, native int)
IL_001e: call bool [System.Core]System.Linq.Enumerable::Any<valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, bool>)
IL_0023: brtrue.s IL_0027
IL_0025: ldc.i4.0
IL_0026: ret
IL_0027: ldc.i4.1
IL_0028: ret
并且 "real" 检查 ID 逻辑在这里(在 <>c__DisplayClass4_0.<checkid2>b__0
下):
IL_0000: ldarga.s t
IL_0002: call instance !0 valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>::get_Key()
IL_0007: ldarg.0
IL_0008: ldfld string ConsoleApp5.Program/'<>c__DisplayClass4_0'::id
IL_000d: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0012: brfalse.s IL_0024
IL_0014: ldarga.s t
IL_0016: call instance !1 valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>::get_Value()
IL_001b: ldfld bool ConsoleApp5.TestObject::deleted
IL_0020: ldc.i4.0
IL_0021: ceq
IL_0023: ret
IL_0024: ldc.i4.0
IL_0025: ret
此代码创建一个新的编译器生成类型 <>c__DisplayClass4.0
,将 id
保存为 class 成员,创建对 <>c__DisplayClass4_0'::'<checkid2>b__0
的委托以获取 KeyValuePair 和 return bool 并调用 Any 以使用此委托。
除此之外,事实是 Any 是 O(n) 而 Dictionary 是 O(1) - 正如 Marcin 所写 - 你得到了答案。
我突然对 linq 性能和 运行 一些测试感到好奇。
下面是我的测试代码,结果非常令人惊讶。
谁能知道 linq 是如何工作的,为什么比 TryOut 慢?
Public class TestObject
{
....
....
//this class contain many members
bool deleted;
....
}
class Program
{
public static ConcurrentDictionary<string, TestObject> testDictionary = new ConcurrentDictionary<string, TestObject>();
static void Main(string[] args)
{
//testDictionary is initialized in ohter code and is likely to have 10000 elements.
RandomTest(0);
RandomTest(1);
Console.ReadKey();
}
static void RandomTest(int k)
{
int count = 10000;
List<string> randomId = new List<string>();
Random rnd = new Random();
for (int i = 0; i < count; i++)
{
int randomNumber = rnd.Next(0, testDictionary.Count());
randomId.Add(testDictionary.ElementAt(randomNumber).key);
}
Stopwatch sw = new Stopwatch();
sw.Start();
if (k == 0)
{
for (int i = 0; i < count; i++)
{
var res = checkid(randomId[i]);
}
}
else if (k == 1)
{
for (int i = 0; i < count; i++)
{
var res = checkid2(randomId[i]);
}
}
sw.Stop();
Console.WriteLine("Elapsed time : " + sw.Elapsed);
}
static bool checkid(string id)
{
TestObject t;
return !testDictionary.TryGetValue(id, out t) ?
false : t.deleted ?
false : true;
}
static bool checkid2(string id)
{
return testDictionary.Any(t => t.key == id && !t.Value.deleted)? true : false;
}
我运行这两种方法10000次,结果如下
checkid方式,大部分用时不到00:00:00.002.
对于checkid2方法,大部分时间在00:00:02.2和00:00:02.4.
之间这是一个巨大的差异。
这是因为checkid2方法即使key不等于Id也会检查删除的变量,而checkid方法只有在找到对应的key时才会检查删除的变量?
Dictionary.TryGetValue
使用散列来定位元素,因此它是一个 O(1) 操作。
Dictionary.Any
将遍历集合以尝试找到符合条件的集合。那是 O(n).
通常 - LINQ 会比使用 for
/foreach
的 hand-crafting 循环慢一点,但在大多数情况下性能差异无关紧要。您在这里遇到的不是 LINQ 变慢,而是 Dictionary<T>.TryGetValue
变快,因为它的内部结构针对 key-based 搜索进行了优化。如果您将其更改为 List<T>
并编写 for
循环以线性方式执行相同的搜索(就像 LINQ 在幕后所做的那样),您会发现差异变得更小。
@MarcinJuraszek 回答正确。但我想添加一些相关的 LINQ 信息来完成答案。是的,散列与迭代行为的主要区别很明显,但是您需要了解更多关于 .NET 中的 LINQ
对于 checkid
,IL 如下所示:
IL_0000: ldsfld class [mscorlib]System.Collections.Concurrent.ConcurrentDictionary`2<string, class ConsoleApp5.TestObject> ConsoleApp5.Program::testDictionary
IL_0005: ldarg.0
IL_0006: ldloca.s t
IL_0008: callvirt instance bool class [mscorlib]System.Collections.Concurrent.ConcurrentDictionary`2<string, class ConsoleApp5.TestObject>::TryGetValue(!0, !1&)
IL_000d: brfalse.s IL_001b
IL_000f: ldloc.0
IL_0010: ldfld bool ConsoleApp5.TestObject::deleted
IL_0015: brtrue.s IL_0019
IL_0017: ldc.i4.1
IL_0018: ret
IL_0019: ldc.i4.0
IL_001a: ret
IL_001b: ldc.i4.0
IL_001c: ret
它完全按照你的想法去做(你在代码中写的)。
但是 checkid2
这样做:
IL_0000: newobj instance void ConsoleApp5.Program/'<>c__DisplayClass4_0'::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld string ConsoleApp5.Program/'<>c__DisplayClass4_0'::id
IL_000d: ldsfld class [mscorlib]System.Collections.Concurrent.ConcurrentDictionary`2<string, class ConsoleApp5.TestObject> ConsoleApp5.Program::testDictionary
IL_0012: ldloc.0
IL_0013: ldftn instance bool ConsoleApp5.Program/'<>c__DisplayClass4_0'::'<checkid2>b__0'(valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>)
IL_0019: newobj instance void class [mscorlib]System.Func`2<valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>, bool>::.ctor(object, native int)
IL_001e: call bool [System.Core]System.Linq.Enumerable::Any<valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, bool>)
IL_0023: brtrue.s IL_0027
IL_0025: ldc.i4.0
IL_0026: ret
IL_0027: ldc.i4.1
IL_0028: ret
并且 "real" 检查 ID 逻辑在这里(在 <>c__DisplayClass4_0.<checkid2>b__0
下):
IL_0000: ldarga.s t
IL_0002: call instance !0 valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>::get_Key()
IL_0007: ldarg.0
IL_0008: ldfld string ConsoleApp5.Program/'<>c__DisplayClass4_0'::id
IL_000d: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0012: brfalse.s IL_0024
IL_0014: ldarga.s t
IL_0016: call instance !1 valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<string, class ConsoleApp5.TestObject>::get_Value()
IL_001b: ldfld bool ConsoleApp5.TestObject::deleted
IL_0020: ldc.i4.0
IL_0021: ceq
IL_0023: ret
IL_0024: ldc.i4.0
IL_0025: ret
此代码创建一个新的编译器生成类型 <>c__DisplayClass4.0
,将 id
保存为 class 成员,创建对 <>c__DisplayClass4_0'::'<checkid2>b__0
的委托以获取 KeyValuePair 和 return bool 并调用 Any 以使用此委托。
除此之外,事实是 Any 是 O(n) 而 Dictionary 是 O(1) - 正如 Marcin 所写 - 你得到了答案。