Самая легковесная коллекция .NET

Скажем, требуется передать IEnumerable в метод. У нас имеется от 2 до 10 элементов. Какой класс коллекции был бы наиболее уместен и удачен в плане производительности, если после передачи коллекции в метод нам больше не потребуется эта коллекция (то есть функционала IEnumerable для нее вполне достаточно)? Вот примерное применение нужной коллекции:

Task.WhenAll(new List<Task> { SomeTask1, SomeTask2, SomeTask3 });

Дополнение: Много интересных вариантов и предложений, спасибо всем)


Ответы (3 шт):

Автор решения: Andrew

В шарпе и List и массив одинаково легковесны. Но с одним уточнением - если лист создавался с предустановленным количеством элементов. Это связано с тем, что в шарпе List является ArrayList - то есть враппером вокруг массива. И если ты задаешь наперед количество элементов - они, фактически, идентичны между собой по легковесности.

C# List vs LinkedList vs Array

→ Ссылка
Автор решения: aepot

Можно вот так Task.WhenAll(SomeTask1, SomeTask2, SomeTask3). А вообще самая легковесная коллекция - это массив, то есть new Task[].

В .NET Core появилась еще одна штука - ArrayPool<T>. Ее можно не только с Task использовать, а вообще с чем угодно. Единственная проблема - вы должны заранее знать минимальный требуемый объем выделяемого вам массива.

ArrayPool<Task> pool = ArrayPool<Task>.Shared;
Task[] tasks = pool.Rent(3);
tasks[0] = ...;
await Tasks.WhenAll(tasks[..3]);
pool.Return(tasks);

В этом случае аллокация массива произойдет только один раз, и далее он будет переиспользоваться. В общем, быстрее дальше некуда.

Но там есть тонкости, пул дает вам массив длиной больше либо равной той, которую вы запросили. Так что конкретно с тасками его может быть и не очень удобно будет использовать, но здесь надо смотреть на то, как себя будет вести Task.WhenAll с недозаполненным массивом, я не проверял. Поэтому я в коде использую System.Range - tasks[..3].

Бонус

Еще один вариант (правда не в тему) - это аллокация массива в стеке, такой фокус позволяет провернуть в .NET Core Span<T>. Но с Task это работать не будет, а будет работать только с неуправляемыми типами - примитивами. И так же, этот фокус нельзя вытворять в асинхронных методах. Так же такой способ быстрой аллокации не подходит для больших массивов, 1 МБ стека (по умолчанию) может легко переполниться, что будет дальше - вы знаете.

Span<byte> span = stackalloc byte[100]; // аллокация в стеке - очень быстро
Span<byte> span = new byte[100]; // обычная аллокация в куче

Подробнее можно прочитать в документации.

→ Ссылка
Автор решения: Alexander Petrov

Всё же напишу ещё один ответ.

Метод Task.WhenAll имеет перегрузки с ключевым словом params. Что позволяет использовать его так:

Task.WhenAll(SomeTask1, SomeTask2, SomeTask3)

То есть не нужно явно создавать коллекцию. Массив будет создан автоматически.

Удобно? Да. Перечисление массива эффективно? Несомненно.


Однако, некоторые статические анализаторы на такой код выдают предупреждение об array allocation - выделении памяти под массив.

Если этот код будет использован однократно, то можно так и оставить.

Но если он будет вызываться многократно, а ваше приложения является высоконагруженным, то следует избавиться от лишней аллокации.
Как раз для этого в новых версиях языка и придуманы ArrayPool и Span + stackalloc. С кожалению, пул выдаст массив с размером 16 элементов. А у WhenAll нет перегрузки, принимающей спан.

Вероятно, проще всего будет создать поле для нашего массива:

private Task[] tasks = new Task[3]; // это именно ПОЛЕ класса, а не локальная переменная!

В методе заполняем его и используем:

tasks[0] = SomeTask1;
tasks[1] = SomeTask2;
tasks[2] = SomeTask3;

await Task.WhenAll(tasks);

В итоге получаем единственную аллокацию на все вызовы.
Но это пригодно если только количество элементов не меняется от вызова к вызову.

→ Ссылка