Страницы

Поиск по вопросам

четверг, 28 ноября 2019 г.

Упаковка ValueType при использовании IEnumerable

#c_sharp #массивы #.net


Допустим, имеется некий массив, например:

int[,] array = { { 1, 2, 3 }, { 4, 5, 6 } };


Все массивы реализуют IEnumerable (не generic), таким образом, при использовании
этого интерфейса все элементы будут упакованы?

Вопрос актуален, например, при использовании Linq-операций Cast() или OfType():

Console.WriteLine(string.Join(" " , array.Cast()));

    


Ответы

Ответ 1



Да, ситуация с многомерными массивами довольно печальная. Такой массив реализует IEnumerable, но не реализует IEnumerable. А это означает, что любое использование многомерного массива через "призму" IEnumerable приведет к упаковке каждого элемента, и использование метода Enumerable.Cast - не исключение. Вот простой бенчмарк (на основе BenchmarkDotNet), который показывает, что это действительно так: [MemoryDiagnoser] public class MultidimentionalAarrayTests { private int[,] m_multiArray = {{1, 2}, {3, 4}}; private int[] m_regularArray = {1, 2, 3, 4}; [Benchmark] public int MultiArrayLast() { return m_multiArray.Cast().Last(); } [Benchmark] public int RegularArrayLast() { return m_regularArray.Last(); } } Результат: Method | Mean | Error | StdDev | Gen 0 | Allocated | --------------------------- |------------:|----------:|----------:|-------:|----------:| MultiArrayLast | 1,166.97 ns | 23.229 ns | 51.473 ns | 0.0401 | 132 B | RegularArrayLast | 51.29 ns | 1.250 ns | 3.686 ns | - | 0 B | Мы тут видим кучку аллокаций: в первом случае - упакован каждый элемент, итератор в Cast, итератор в Last. Во втором случае нет аллокаций вообще, поскольку Last проверяет, что последовательность реализует IList (а одномерный массив его реализует) и сразу же возвращает последний элемент. Поскольку многомерные массивы не реализуют обобщенный IEnumerable, то заставить его сделать это самим мы не можем, но мы можем создать метод расширения, чтобы не использовать Enumerable.Cast: public static class MultiDimentionalArrayEx { public static IEnumerable AsEnumerable(this T[,] array) { foreach (var e in array) yield return e; } } Теперь мы можем добавить еще один бенчмарк, чтобы проверить результат: [Benchmark] public int MultiArrayWithAsEnumerable() { return m_multiArray.AsEnumerable().Last(); } И вот окончательный результат: Method | Mean | Error | StdDev | Gen 0 | Allocated | --------------------------- |------------:|-----------:|-----------:|-------:|----------:| MultiArrayLast | 1,115.45 ns | 31.0145 ns | 90.9603 ns | 0.0401 | 132 B | RegularArrayLast | 46.11 ns | 0.1826 ns | 0.1525 ns | - | 0 B | MultiArrayWithAsEnumerable | 161.74 ns | 3.2693 ns | 3.2109 ns | 0.0150 | 48 B | Здесь мы видим, что есть выделение в куче двух итераторов (одного для метода расширения и еще одного для Enumerable.Last), но нет упаковок самих элементов.

Ответ 2



При использовании необобщённого IEnumerable упаковки, конечно, не избежать. Но компилятор умный, и в некоторых случаях может обойтись без IEnumerable. Важный случай — это если объект, по которому производится перечисление, обладает открытым методом GetEnumerator с подходящей сигнатурой. В этом случае будет использован именно он.* Второй важный частный случай (и именно он у нас имеет место) — это массивы. Компилятор знает, как можно более эффективно обходить массивы, и иногда пользуется этим. Например, вот такая функция static int[,] array = ...; static void Test() { foreach (var val in array) Console.WriteLine(val); } скомпилировалась так, как будто она была написана следующим образом: int[,] array = Program.array; int upperBound = array.GetUpperBound(0); int upperBound2 = array.GetUpperBound(1); for (int i = array.GetLowerBound(0); i <= upperBound; i++) { for (int j = array.GetLowerBound(1); j <= upperBound2; j++) { Console.WriteLine(array[i, j]); } } Для случая Cast, кажется, оптимизатор не пытается улучшить код для массивов, и таки использует Cast. В коде Cast есть проверка на наличие типизированного варианта IEnumerable (и в этом случае упаковки бы не было), но массив его не поддерживает. Так что выполняется итерация по IEnumerable с упаковкой результатов. В последующих версиях языка, возможно, оптимизатор станет умнее (если разработчики сочтут этот случай важным). (Для недоверчивых, вот IL-код: // int[,] array = Program.array; IL_0000: ldsfld int32[0..., 0...] Test.Program::'array' IL_0005: stloc.0 // int upperBound = array.GetUpperBound(0); IL_0006: ldloc.0 IL_0007: ldc.i4.0 IL_0008: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32) IL_000d: stloc.1 // int upperBound2 = array.GetUpperBound(1); IL_000e: ldloc.0 IL_000f: ldc.i4.1 IL_0010: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32) IL_0015: stloc.2 // i = array.GetLowerBound(0) IL_0016: ldloc.0 IL_0017: ldc.i4.0 IL_0018: callvirt instance int32 [mscorlib]System.Array::GetLowerBound(int32) IL_001d: stloc.3 IL_001e: br.s IL_0048 // jump to outer loop check // loop start (head: IL_0048) // j = array.GetLowerBound(1) IL_0020: ldloc.0 IL_0021: ldc.i4.1 IL_0022: callvirt instance int32 [mscorlib]System.Array::GetLowerBound(int32) IL_0027: stloc.s 4 IL_0029: br.s IL_003f // jump to inner loop check // loop start (head: IL_003f) // array[i, j] IL_002b: ldloc.0 IL_002c: ldloc.3 IL_002d: ldloc.s 4 IL_002f: call instance int32 int32[0..., 0...]::Get(int32, int32) IL_0034: call void [mscorlib]System.Console::WriteLine(int32) // j++ IL_0039: ldloc.s 4 IL_003b: ldc.i4.1 IL_003c: add IL_003d: stloc.s 4 // j <= upperBound2 IL_003f: ldloc.s 4 IL_0041: ldloc.2 IL_0042: ble.s IL_002b // end loop // i++ IL_0044: ldloc.3 IL_0045: ldc.i4.1 IL_0046: add IL_0047: stloc.3 // i <= upperBound IL_0048: ldloc.3 IL_0049: ldloc.1 IL_004a: ble.s IL_0020 // end loop IL_004c: ret Проверяйте!) *Ссылка на документацию: Otherwise, determine whether the type X has an appropriate GetEnumerator method: и только после этого Otherwise, check for an enumerable interface: Это позволяет, в частности, при итерации по List итерировать не по интерфейсу IEnumerator, а по структуре List.Enumerator, и тем самым избежать упаковки этой структуры.

Комментариев нет:

Отправить комментарий