如何在 IL 中实现 C# foreach 优化
How to implement C# foreach optimization in IL
在此 answer and this GitHub issue(顶部项目)中描述了 C# 编译器使用的 foreach
优化。
基本上,生成的代码不是分配 IEnumerable<T>
,而是调用 GetEnumerator()
,然后在返回的对象上调用 MoveNext()
,始终使用直接 call
,因此避免装箱和虚拟电话。
是否可以用中间语言写出相同的逻辑?我是 IL 的初学者,但是熟悉 Unsafe package 及其工作方式。我想知道是否可以在 IL 中编写一个 unsafe 方法来接受一些对象并直接调用它的方法和属性?
(此外,有人可以给 Roslyn repo 中发生此 foreach
优化的行添加 link 吗?该回购协议是如此庞大和复杂,我在那里迷路了到目前为止。)
更新:
这是一个方法模板
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ILSub(@"
.. IL code here to be replaced by ilasm.exe
.. Is there a way to do the same without boxing and virtual calls?
")]
public T CallIEnumerableMoveNextViaIL<T>(IEnumerable<T> enumerable)
{
// I know that the `enumerable` returns an enumerator that is a struct, but its type could be custom
// Next two calls are virtual via an interface, and enumerator is boxed
var enumerator = enumerable.GetEnumerator();
enumerator.MoveNext();
return enumerator.Current;
}
这里是该方法的 IL:
IL_0000: ldarg.1
IL_0001: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<!!T>::GetEnumerator()
IL_0006: dup
IL_0007: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_000c: pop
IL_000d: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<!!T>::get_Current()
IL_0012: ret
从评论看来,在不知道类型的情况下调用 get_Current()
等方法是不可能的。
让我们举几个例子来说明 foreach
是如何编译的。
首先,在常规 IEnumerator 上返回 GetEnumerator:
public class A : IEnumerable
{
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
foreach(object o in new A())
{
}
.locals init (class [mscorlib]System.Collections.IEnumerator V_0,
class [mscorlib]System.IDisposable V_1)
IL_0000: newobj instance void Testing.Program/A::.ctor()
IL_0005: call instance class [mscorlib]System.Collections.IEnumerator Testing.Program/A::GetEnumerator()
IL_000a: stloc.0
.try
{
IL_000b: br.s IL_0014
IL_000d: ldloc.0
IL_000e: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
IL_0013: pop
IL_0014: ldloc.0
IL_0015: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001a: brtrue.s IL_000d
IL_001c: leave.s IL_002f
} // end .try
finally
{
IL_001e: ldloc.0
IL_001f: isinst [mscorlib]System.IDisposable
IL_0024: stloc.1
IL_0025: ldloc.1
IL_0026: brfalse.s IL_002e
IL_0028: ldloc.1
IL_0029: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_002e: endfinally
} // end handler
这里没什么了不起的,只是调用 IEnumerator 上的方法。请注意,它还实现了 IDisposable,因此它使用了它的模式。
接下来,让我们有一个值type-returning GetEnumerator:
public class B
{
public BE GetEnumerator()
{
return new BE();
}
public struct BE
{
public object Current {
get {
throw new NotImplementedException();
}
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}
}
.locals init (class [mscorlib]System.Collections.IEnumerator V_0,
class [mscorlib]System.IDisposable V_1,
valuetype Testing.Program/B/BE V_2,
object[] V_3,
int32 V_4)
IL_002f: newobj instance void Testing.Program/B::.ctor()
IL_0034: call instance valuetype Testing.Program/B/BE Testing.Program/B::GetEnumerator()
IL_0039: stloc.2
IL_003a: br.s IL_0044
IL_003c: ldloca.s V_2
IL_003e: call instance object Testing.Program/B/BE::get_Current()
IL_0043: pop
IL_0044: ldloca.s V_2
IL_0046: call instance bool Testing.Program/B/BE::MoveNext()
IL_004b: brtrue.s IL_003c
请注意,这里没有任何内容必须实现 IEnumerable/IEnumerator 接口。此方法有时称为 duck-typing。此实现在内部将枚举器存储在一个变量中,然后调用其地址 (ldloca
) 上的方法。当从 GetEnumerator.
返回枚举器时,这里发生一种值类型复制
第三个例子几乎完全不同,foreach
在数组上:
foreach(object o in new object[0])
{
}
.locals init (class [mscorlib]System.Collections.IEnumerator V_0,
class [mscorlib]System.IDisposable V_1,
valuetype Testing.Program/B/BE V_2,
object[] V_3,
int32 V_4) IL_004d: ldc.i4.0
IL_004e: newarr [mscorlib]System.Object
IL_0053: stloc.3
IL_0054: ldc.i4.0
IL_0055: stloc.s V_4
IL_0057: br.s IL_0064
IL_0059: ldloc.3
IL_005a: ldloc.s V_4
IL_005c: ldelem.ref
IL_005d: pop
IL_005e: ldloc.s V_4
IL_0060: ldc.i4.1
IL_0061: add
IL_0062: stloc.s V_4
IL_0064: ldloc.s V_4
IL_0066: ldloc.3
IL_0067: ldlen
IL_0068: conv.i4
IL_0069: blt.s IL_0059
这里没有使用GetEnumerator,只是简单地以old-fashioned索引方式遍历数组(性能更高)。
您不能在 CIL 中使用这种精确优化,因为 CIL 没有 duck-typing;您必须自己编写所有签名和方法调用。
但是,如果您需要对任何类型进行此优化,并且可以修改要使用的类型,则可以在类似于以下的代码中使用泛型接口:
public class B : IStructEnumerable<object, BE>
{
public BE GetEnumerator()
{
return new BE();
}
}
public struct BE : IStructEnumerator<object>
{
public object Current {
get {
throw new NotImplementedException();
}
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}
public interface IStructEnumerable<TItem, TEnumerator> where TEnumerator : struct, IStructEnumerator<TItem>
{
TEnumerator GetEnumerator();
}
public interface IStructEnumerator<TItem>
{
TItem Current {get;}
bool MoveNext();
void Reset();
}
public static void TestEnumerator<TEnumerable, TEnumerator>(TEnumerable b) where TEnumerable : IStructEnumerable<object, TEnumerator> where TEnumerator : struct, IStructEnumerator<object>
{
foreach(object obj in b)
{
}
}
在此 answer and this GitHub issue(顶部项目)中描述了 C# 编译器使用的 foreach
优化。
基本上,生成的代码不是分配 IEnumerable<T>
,而是调用 GetEnumerator()
,然后在返回的对象上调用 MoveNext()
,始终使用直接 call
,因此避免装箱和虚拟电话。
是否可以用中间语言写出相同的逻辑?我是 IL 的初学者,但是熟悉 Unsafe package 及其工作方式。我想知道是否可以在 IL 中编写一个 unsafe 方法来接受一些对象并直接调用它的方法和属性?
(此外,有人可以给 Roslyn repo 中发生此 foreach
优化的行添加 link 吗?该回购协议是如此庞大和复杂,我在那里迷路了到目前为止。)
更新:
这是一个方法模板
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ILSub(@"
.. IL code here to be replaced by ilasm.exe
.. Is there a way to do the same without boxing and virtual calls?
")]
public T CallIEnumerableMoveNextViaIL<T>(IEnumerable<T> enumerable)
{
// I know that the `enumerable` returns an enumerator that is a struct, but its type could be custom
// Next two calls are virtual via an interface, and enumerator is boxed
var enumerator = enumerable.GetEnumerator();
enumerator.MoveNext();
return enumerator.Current;
}
这里是该方法的 IL:
IL_0000: ldarg.1
IL_0001: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<!!T>::GetEnumerator()
IL_0006: dup
IL_0007: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_000c: pop
IL_000d: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<!!T>::get_Current()
IL_0012: ret
从评论看来,在不知道类型的情况下调用 get_Current()
等方法是不可能的。
让我们举几个例子来说明 foreach
是如何编译的。
首先,在常规 IEnumerator 上返回 GetEnumerator:
public class A : IEnumerable
{
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
foreach(object o in new A())
{
}
.locals init (class [mscorlib]System.Collections.IEnumerator V_0,
class [mscorlib]System.IDisposable V_1)
IL_0000: newobj instance void Testing.Program/A::.ctor()
IL_0005: call instance class [mscorlib]System.Collections.IEnumerator Testing.Program/A::GetEnumerator()
IL_000a: stloc.0
.try
{
IL_000b: br.s IL_0014
IL_000d: ldloc.0
IL_000e: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
IL_0013: pop
IL_0014: ldloc.0
IL_0015: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001a: brtrue.s IL_000d
IL_001c: leave.s IL_002f
} // end .try
finally
{
IL_001e: ldloc.0
IL_001f: isinst [mscorlib]System.IDisposable
IL_0024: stloc.1
IL_0025: ldloc.1
IL_0026: brfalse.s IL_002e
IL_0028: ldloc.1
IL_0029: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_002e: endfinally
} // end handler
这里没什么了不起的,只是调用 IEnumerator 上的方法。请注意,它还实现了 IDisposable,因此它使用了它的模式。
接下来,让我们有一个值type-returning GetEnumerator:
public class B
{
public BE GetEnumerator()
{
return new BE();
}
public struct BE
{
public object Current {
get {
throw new NotImplementedException();
}
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}
}
.locals init (class [mscorlib]System.Collections.IEnumerator V_0,
class [mscorlib]System.IDisposable V_1,
valuetype Testing.Program/B/BE V_2,
object[] V_3,
int32 V_4)
IL_002f: newobj instance void Testing.Program/B::.ctor()
IL_0034: call instance valuetype Testing.Program/B/BE Testing.Program/B::GetEnumerator()
IL_0039: stloc.2
IL_003a: br.s IL_0044
IL_003c: ldloca.s V_2
IL_003e: call instance object Testing.Program/B/BE::get_Current()
IL_0043: pop
IL_0044: ldloca.s V_2
IL_0046: call instance bool Testing.Program/B/BE::MoveNext()
IL_004b: brtrue.s IL_003c
请注意,这里没有任何内容必须实现 IEnumerable/IEnumerator 接口。此方法有时称为 duck-typing。此实现在内部将枚举器存储在一个变量中,然后调用其地址 (ldloca
) 上的方法。当从 GetEnumerator.
第三个例子几乎完全不同,foreach
在数组上:
foreach(object o in new object[0])
{
}
.locals init (class [mscorlib]System.Collections.IEnumerator V_0,
class [mscorlib]System.IDisposable V_1,
valuetype Testing.Program/B/BE V_2,
object[] V_3,
int32 V_4) IL_004d: ldc.i4.0
IL_004e: newarr [mscorlib]System.Object
IL_0053: stloc.3
IL_0054: ldc.i4.0
IL_0055: stloc.s V_4
IL_0057: br.s IL_0064
IL_0059: ldloc.3
IL_005a: ldloc.s V_4
IL_005c: ldelem.ref
IL_005d: pop
IL_005e: ldloc.s V_4
IL_0060: ldc.i4.1
IL_0061: add
IL_0062: stloc.s V_4
IL_0064: ldloc.s V_4
IL_0066: ldloc.3
IL_0067: ldlen
IL_0068: conv.i4
IL_0069: blt.s IL_0059
这里没有使用GetEnumerator,只是简单地以old-fashioned索引方式遍历数组(性能更高)。
您不能在 CIL 中使用这种精确优化,因为 CIL 没有 duck-typing;您必须自己编写所有签名和方法调用。
但是,如果您需要对任何类型进行此优化,并且可以修改要使用的类型,则可以在类似于以下的代码中使用泛型接口:
public class B : IStructEnumerable<object, BE>
{
public BE GetEnumerator()
{
return new BE();
}
}
public struct BE : IStructEnumerator<object>
{
public object Current {
get {
throw new NotImplementedException();
}
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}
public interface IStructEnumerable<TItem, TEnumerator> where TEnumerator : struct, IStructEnumerator<TItem>
{
TEnumerator GetEnumerator();
}
public interface IStructEnumerator<TItem>
{
TItem Current {get;}
bool MoveNext();
void Reset();
}
public static void TestEnumerator<TEnumerable, TEnumerator>(TEnumerable b) where TEnumerable : IStructEnumerable<object, TEnumerator> where TEnumerator : struct, IStructEnumerator<object>
{
foreach(object obj in b)
{
}
}