【问题标题】:How to implement C# foreach optimization in IL如何在 IL 中实现 C# foreach 优化
【发布时间】: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&lt;int&gt;.Enumerator”,然后 you 去“哦,这里使用的类型是 List&lt;int&gt;.Enumerator” .让 IL 本身实际处理它就像让 C# 本身实际处理它,这更类似于例如如何处理它。一些 linq 方法检查 IEnumerable 的类型并选择针对该类型优化的路径。
  • @Andrey 只是因为 .NET 是如何工作的 - 一个接口不会透露其底层类型。

标签: c# cil


【解决方案1】:

让我们举几个例子来说明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,因此它使用了它的模式。

接下来,让我们有一个值类型返回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 接口。这种方法有时被称为鸭式打字。此实现在内部将枚举数存储在一个变量中,然后在其地址 (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,只是以老式的索引方式遍历数组(性能更高)。

你不能在 CIL 中使用这种精确的优化,因为 CIL 没有鸭子类型;您必须自己编写所有签名和方法调用。

但是,如果你需要对任何类型进行这种优化,并且你可以修改你想要使用的类型,你可以在类似这样的代码中使用泛型接口:

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)
    {

    }
}

【讨论】:

  • 谢谢! “因为 CIL 没有鸭式打字”是我不确定的事情。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-26
  • 2019-06-23
  • 2016-11-17
  • 1970-01-01
  • 2015-09-03
  • 2017-07-30
相关资源
最近更新 更多