2017-02-15 20 views
2

В этом answer и этом GitHub issue (верхний элемент) приводится описание оптимизации foreach, используемой компилятором C#.Как реализовать оптимизацию foreach C# в IL

В принципе, вместо выделения IEnumerable<T>, сгенерированный код вызывает GetEnumerator(), а затем MoveNext() на возвращенном объекте, всегда используя прямой call и, следовательно, избежать бокс и виртуальных вызовов.

Можно ли написать ту же логику на языке посредника? Я довольно новичок в IL, но знаком с Unsafe package и тем, как он работает. Интересно, можно ли написать небезопасный метод в IL, который принимает некоторый объект и вызывает его методы и свойства напрямую?

(? Кроме того, может кто-то любезно дать ссылку на линии в Roslyn repo, где это foreach оптимизация происходит РЭПО является настолько большим и сложным, я потерял там до сих пор.)

Update:

Вот шаблон метод

[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(), не зная тип.

+0

Интересный вопрос. Хотя я не уверен, как вы представляете небезопасный метод CIL, который использует возвращаемый тип значения * GetEnumerator *. Вы можете вызвать метод с помощью указателя, но вам все равно нужно знать тип, который он возвращает для CLR, чтобы работать с возвращаемым значением в стеке. * Reflection.Emit * - лучший друг. – IllidanS4

+0

@ IllidanS4 Я хочу вызвать такой метод, когда я уверен, что входной объект реализует этот контракт, и вопрос, можно ли вызвать метод на объект пользовательского типа по имени, используя IL? Вместо Reflection.Emit я могу написать метод в IL и использовать ilasm.exe, чтобы я мог вызвать сгенерированный метод напрямую (не через делегат). –

+1

Нет, вы не можете вызвать метод по имени в CIL. После компиляции CIL все привязки методов разрешены к конкретным методам метода, а не к именам. CIL не использует контракты, вы должны указать все, что используете. Теоретически вы * можете * представлять такой вызов с помощью общего метода, если вы передаете тип, возвращаемый из * GetEnumerator *, как аргумент общего типа и используете его как возвращаемый тип, но я сомневаюсь, что он даже скомпилировался. CLR должен знать размер возвращаемого типа при вызове такого метода. – IllidanS4

ответ

5

Давайте рассмотрим несколько примеров того, как foreach скомпилируется.
Во-первых, на регулярной IEnumerator -returning 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) 
    { 

    } 
} 
+0

Спасибо! «потому что у CIL нет утиного набора» - это то, что я не был уверен. –