如何在IL中实现C#foreach优化

时间:2017-02-15 14:38:12

标签: c# cil

在此answer和此GitHub issue(顶部项目)中,描述了C#编译器使用的foreach优化。

基本上,生成的代码不是分配IEnumerable<T>,而是在返回的对象上调用GetEnumerator()然后MoveNext(),总是使用直接call,因此避免装箱和虚拟调用

是否可以用中间语言编写相同的逻辑?我是IL的初学者,但熟悉Unsafe package及其工作方式。我想知道是否有可能在IL中编写一个 unsafe 方法,它接受一些对象并直接调用它的方法和属性?

(另外,有人可以提供Roslyn repoforeach优化发生的行的链接吗?回购是如此之大和复杂,到目前为止我在那里迷失了。)

更新

这是一个方法模板

[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()等方法是不可能的。

1 个答案:

答案 0 :(得分:5)

让我们举几个例子来说明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 接口。此方法有时称为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 ,只是以旧式索引方式遍历数组(性能更高)。

你不能在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)
    {

    }
}