В предыдущем посте я писал о скрытой аллокации, возникающей из-за упаковки энумератора. В этом посте расскажу об ещё одной скрытой аллокации.
5 лет назад в C# 8.0 появились индексы и диапазоны, которые позволяют получать часть коллекции с помощью удобного синтаксиса. Например, в следующем примере из массива извлекаются все элементы, кроме 2-х первых и 2-х последних:
int[] arr = [1, 2, 3, 4, 5, 6];
var subarr = arr[2..^2]; // [3, 4]
Это удобно, особенно по сравнению с аналогичной записью через LINQ:
var subarr = arr.Skip(2)
.Take(arr.Length - 2 - 2)
.ToArray();
В случае с LINQ мы явно создаём массив с помощью метода ToArray. Однако это можно и не делать, если нужно просто пройтись по выбранным элементам.
Теперь посмотрим на C#-код без синтаксического сахара. При использовании с массивом оператор диапазона всегда создаёт новый массив. При компиляции он преобразуется в вызов RuntimeHelpers.GetSubArray, который и создаёт новый массив.
// int[] subarr = arr[2..^2]
int[] subArray = RuntimeHelpers
.GetSubArray(array, new Range(2,
new Index(2, true)));
Если нет необходимости сохранять подмассив, лучше вызвать AsSpan() перед использованием диапазонов.
// Исходный код
int[] arr = [1, 2, 3, 4, 5, 6];
var subarr = arr.AsSpan()[2..^2]; // [3, 4]
// После компиляции
Span<int> span = MemoryExtensions.AsSpan(array);
Span<int> subarr = span.Slice(2,
span.Length - 2 - 2);
С точки зрения производительности, создание нового массива и копирование элементов – это затратная операция, особенно в сравнении со слайсами спанов, которые выполняются практически мгновенно. Рассмотрим следующий код:
start = Length / 4;
end = Length * 3 / 4;
var sum = 0;
// 2-3 times faster:
// _transactionsArray.AsSpan()[_start.._end];
var sliced = _transactionsArray[start..end];
foreach (var t in sliced) sum += t.Amount;
return sum;
Добавление AsSpan() перед использованием оператора диапазона позволяет сократить время выполнения в среднем в 2-3 раза (см. график).
