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

300
29 октября 2017, 16:51

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

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

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

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

Console.WriteLine(string.Join(" " , array.Cast<int>()));
Answer 1

Да, ситуация с многомерными массивами довольно печальная. Такой массив реализует IEnumerable, но не реализует IEnumerable<T>. А это означает, что любое использование многомерного массива через "призму" IEnumerable приведет к упаковке каждого элемента, и использование метода Enumerable.Cast<T> - не исключение.

Вот простой бенчмарк (на основе 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<int>().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<T>, итератор в Last<T>. Во втором случае нет аллокаций вообще, поскольку Last<T> проверяет, что последовательность реализует IList<T> (а одномерный массив его реализует) и сразу же возвращает последний элемент.

Поскольку многомерные массивы не реализуют обобщенный IEnumerable<T>, то заставить его сделать это самим мы не можем, но мы можем создать метод расширения, чтобы не использовать Enumerable.Cast<T>:

public static class MultiDimentionalArrayEx
{
    public static IEnumerable<T> AsEnumerable<T>(this T[,] array) where T:struct
    {
        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<T>), но нет упаковок самих элементов.

Answer 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<T>, кажется, оптимизатор не пытается улучшить код для массивов, и таки использует Cast. В коде Cast<T> есть проверка на наличие типизированного варианта IEnumerable<T> (и в этом случае упаковки бы не было), но массив его не поддерживает. Так что выполняется итерация по 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<T> итерировать не по интерфейсу IEnumerator<T>, а по структуре List<T>.Enumerator, и тем самым избежать упаковки этой структуры.

READ ALSO
JS функция не срабатывает [дубликат]

JS функция не срабатывает [дубликат]

На данный вопрос уже ответили:

279
History - событие popstate

History - событие popstate

Задача следующая:

337
id is not defined

id is not defined

Каждому подключенному модулю задан id с соответствующим именемПри нажатии на ссылку нужно узнавать id этой ссылки, и загружать модуль с названием...

320
vk.com какая функция отвечает за мигание иконки сайта, когда сообщение приходит вне вкладки?

vk.com какая функция отвечает за мигание иконки сайта, когда сообщение приходит вне вкладки?

Нужно изменить/отключить функцию, которая ответственна за постоянную смену значения в адресе documentquerySelectorAll("link")[0]

317