#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, и тем самым избежать упаковки этой структуры.
Комментариев нет:
Отправить комментарий