【发布时间】:2023-03-13 00:00:01
【问题描述】:
在这个answer 和这个GitHub issue(顶部项目)中有一个关于C# 编译器使用的foreach 优化的描述。
基本上,生成的代码不是分配IEnumerable<T>,而是调用GetEnumerator(),然后在返回的对象上调用MoveNext(),始终使用直接call,因此避免了装箱和虚拟调用。
是否可以用中介语言编写相同的逻辑?我是 IL 的初学者,但我熟悉 Unsafe package 及其工作方式。我想知道是否可以在 IL 中编写一个 unsafe 方法来接受一些对象并直接调用它的方法和属性?
(另外,有人可以提供Roslyn repo 中发生此foreach 优化的行的链接吗?回购是如此庞大和复杂,到目前为止我迷路了。)
更新:
这是一个方法模板
[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
从 cmets 看来,在不知道类型的情况下调用诸如 get_Current() 之类的方法是不可能的。
【问题讨论】:
-
不,您不能在 CIL 中按名称调用方法。在 CIL 编译之后,所有方法绑定都被解析为特定的方法标记,而不是名称。 CIL 不使用合同,您必须指定您使用的所有内容。理论上,如果您将 GetEnumerator 返回的类型作为泛型类型参数传递并将其用作返回类型,则您可以 用泛型方法表示此类调用,但我怀疑甚至可以编译。调用此类方法时,CLR 需要知道返回类型的大小。
-
这是将
foreach降低为等效循环的代码:github.com/dotnet/roslyn/blob/… -
“我知道
enumerable返回一个结构体枚举数,但它的类型可以是自定义的” 这与 C# 优化的情况不同。它确实知道确切的类型。实际上,C# 对其进行优化并没有那么多,因为正常的 C# 行为是在foreaching 的任何类型上查找GetEnumerator(),然后在任何类型上查找MoveNext()和Current结果(foreach实际上并不关心IEnumerable接口)。这自然会提供最强类型的实现。 -
“相同的结果”是 IL(也是可验证的)。直接等效的是,不是 C# 编译器去“哦,这里使用的类型是(例如)
List<int>.Enumerator”,然后 you 去“哦,这里使用的类型是List<int>.Enumerator” .让 IL 本身实际处理它就像让 C# 本身实际处理它,这更类似于例如如何处理它。一些 linq 方法检查IEnumerable的类型并选择针对该类型优化的路径。 -
@Andrey 只是因为 .NET 是如何工作的 - 一个接口不会透露其底层类型。