通过接口枚举 - 性能损失
本文关键字:性能 损失 枚举 接口 | 更新日期: 2023-09-27 18:34:23
我与我的同事发生了一点争论(非常接近圣战:)关于通过 indeces VS 通过枚举器访问列表的性能。为了操作一些事实,我编写了以下测试:
static void Main(string[] args)
{
const int count = 10000000;
var stopwatch = new Stopwatch();
var list = new List<int>(count);
var rnd = new Random();
for (int i = 0; i < count; i++)
{
list.Add( rnd.Next());
}
const int repeat = 20;
double indeces = 0;
double forEach = 0;
for (int iteration = 0; iteration < repeat; iteration++)
{
stopwatch.Restart();
long tmp = 0;
for (int i = 0; i < count; i++)
{
tmp += list[i];
}
indeces += stopwatch.Elapsed.TotalSeconds;
stopwatch.Restart();
foreach (var integer in list)
{
tmp += integer;
}
forEach += stopwatch.Elapsed.TotalSeconds;
}
Console.WriteLine(indeces /repeat);
Console.WriteLine(forEach /repeat);
}
实际上,它只是访问元素。
正如我所料,索引访问速度更快。以下是在我的计算机上发布版本的结果:
0.0347//index access
0.0737//enumerating
但是,我决定稍微改变一下测试:
//the same as before
...
IEnumerable<int> listAsEnumerable = list;
//the same as before
...
foreach (var integer in listAsEnumerable)
{
tmp += integer;
}
...
现在输出如下:
0.0321//index access
0.1246//enumerating (2x slower!)
如果我们通过接口枚举相同的列表,则性能会慢 2 倍!
为什么会这样*?
这意味着"通过接口枚举比枚举实际列表慢 2 倍"。
我的猜测是运行时使用不同的Enumerator
:列表在第一个测试中,一个通用列表在第二个测试中。
当使用List<T>
时,foreach
实际上并不使用 IEnumerable<T>
接口;相反,它使用 List<T>.Enumerator
,这是一个struct
。在微不足道的层面上,这意味着间接性稍微减少 - 不必取消引用,使用静态调用而不是虚拟调用 - 以及更直接的实现。
非常小,在任何合理的现实生活中,差异都是噪声。但是,如果仅测试foreach
性能,则可能会稍微引起注意。
扩展一下:foreach
实际上不需要IEnumerable[<T>]
- 它可以纯粹在GetEnumerator()
/.MoveNext()
/.Current
/.Dispose()
模式上工作;这在2.0的泛型之前尤其重要。
但是,仅当变量类型为 List<T>
(具有自定义 GetEnumerator()
方法)时,这才有可能。一旦你有了IEnumerable<T>
,它必须使用IEnumerator<T>
你可以在这里看到代码:
static void Main()
{
List<int> list = new List<int>(Enumerable.Range(1,10000));
int total = 0;
foreach (var i in list)
{
total += i;
}
IEnumerable<int> enumerable = list;
foreach (var i in enumerable)
{
total += i;
}
Console.ReadLine();
}
这会生成此 IL。请注意
System.Collections.Generic.List`1/Enumerator<int32>
和
System.Collections.Generic.IEnumerable`1<int32>
请注意,它是一个ValueType
(结构):
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 146 (0x92)
.maxstack 2
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> list,
[1] int32 total,
[2] int32 i,
[3] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> enumerable,
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
[5] bool CS$4$0001,
[6] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0002)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: ldc.i4 0x2710
IL_0007: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_000c: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<!0>)
IL_0011: stloc.0
IL_0012: ldc.i4.0
IL_0013: stloc.1
IL_0014: nop
IL_0015: ldloc.0
IL_0016: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_001b: stloc.s CS$5$0000
.try
{
IL_001d: br.s IL_002d
IL_001f: ldloca.s CS$5$0000
IL_0021: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0026: stloc.2
IL_0027: nop
IL_0028: ldloc.1
IL_0029: ldloc.2
IL_002a: add
IL_002b: stloc.1
IL_002c: nop
IL_002d: ldloca.s CS$5$0000
IL_002f: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0034: stloc.s CS$4$0001
IL_0036: ldloc.s CS$4$0001
IL_0038: brtrue.s IL_001f
IL_003a: leave.s IL_004b
} // end .try
finally
{
IL_003c: ldloca.s CS$5$0000
IL_003e: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0044: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0049: nop
IL_004a: endfinally
} // end handler
IL_004b: nop
IL_004c: ldloc.0
IL_004d: stloc.3
IL_004e: nop
IL_004f: ldloc.3
IL_0050: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0055: stloc.s CS$5$0002
.try
{
IL_0057: br.s IL_0067
IL_0059: ldloc.s CS$5$0002
IL_005b: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0060: stloc.2
IL_0061: nop
IL_0062: ldloc.1
IL_0063: ldloc.2
IL_0064: add
IL_0065: stloc.1
IL_0066: nop
IL_0067: ldloc.s CS$5$0002
IL_0069: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_006e: stloc.s CS$4$0001
IL_0070: ldloc.s CS$4$0001
IL_0072: brtrue.s IL_0059
IL_0074: leave.s IL_008a
} // end .try
finally
{
IL_0076: ldloc.s CS$5$0002
IL_0078: ldnull
IL_0079: ceq
IL_007b: stloc.s CS$4$0001
IL_007d: ldloc.s CS$4$0001
IL_007f: brtrue.s IL_0089
IL_0081: ldloc.s CS$5$0002
IL_0083: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0088: nop
IL_0089: endfinally
} // end handler
IL_008a: nop
IL_008b: call string [mscorlib]System.Console::ReadLine()
IL_0090: pop
IL_0091: ret
} // end of method Program2::Main
如果您查看两个版本的 IL,您会发现第一个版本使用 System.Collections.Generic.List<System.Int32>+Enumerator
类型的迭代器 - 嵌套struct
,它针对迭代列表进行了优化。
第二个版本使用System.Collections.Generic.IEnumerator<System.Int32>
的通用实现,效率较低,因为它不会通过保留列表中当前项目的私有索引来"作弊"。
我怀疑使用 for 而不是 foreach 会有性能提升(至少对于原始类型)。据我所知,如果您在同一个数组上执行 for 和 foreach,它们几乎是等效的(不是任何其他结构,如列表,这本身会产生一些开销)。
foreach 和 for 的性能取决于您运行 foreach 和 foreach 的结构类型。
请检查;对于和 Foreach 比较