Стоит ли проверять коллекцию на наличие элементов до вызова цепочки асинхронных функций?
Не нашёл подходящей темы, может плохо искал, но тем не менее. Может кто-то копал такую тему. Вот есть высоконагруженное приложение и хочется его оптимизировать по возможности. В частности есть в нём такие фрагменты кода:
{
var items = GetItemsFromSomethere();
await SomeAsyncFunction(items);
// ...
}
async Task SomeAsyncFunction(ICollection<T> items)
{
var filteredItems = await SomeAsyncFilter(items);
await AnotherAsyncFunction(filteredItems);
// и т.д., там и дальше могут ещё быть цепочки асинхронных вызовов
}
Так вот вопрос такой. Велики ли накладные расходы при вызове всех этих асинхронных функций - ведь там создаётся state machine на каждый вызов и т.д.
При этом я точно знаю, что часто бывают случаи, когда SomeAsyncFunction возвращает пустую коллекцию, да и вызывается она довольно часто и можно было бы избежать вызова всей этой цепочки асинхронных функций, просто проверив что коллекция пустая перед вызовом функции.
{
var items = GetItemsFromSomethere();
if(items.Count > 0)
{
await SomeAsyncFunction(items);
}
// ...
}
async Task SomeAsyncFunction(ICollection<T> items)
{
var filteredItems = await SomeAsyncFilter(items);
if(filteredItems.Count > 0)
{
await AnotherAsyncFunction(filteredItems);
// и т.д., там и дальше могут ещё быть цепочки асинхронных вызовов
}
}
Вроде и можно было бы так сделать везде по коду не задумываясь, ну а вдруг это ничего не даёт, так зачем усложнять и раздувать код какими-то лишними конструкциями. Хотелось бы понять - сэкономлю я что-то при таком подходе хотя бы теоретически или нет? Конечно, это будет зависеть от того, как часто коллекция бывает пустой, насколько длинные цепочки асинхронных функций и т.д., но хотя бы примерно и в общих чертах хотелось бы осознать тему. И второе - может есть какой-то синтаксический сахар, который позволил бы делать такую проверку не перегружая код лишними конструкциями (в случае если такую проверку лучше и правда добавить в код)? Спасибо.
P.S. Что касается синхронных функций, там я так понимаю расходы на лишний вызов функции с пустой коллекцией будут не так уж велики - положить ссылку на коллекцию в стек, да вызвать функцию. Всё-таки не state machine, попроще дело. Но если и насчёт синхронных функций есть какие-то рецепты на тему, с удовольствием их послушаю.
Update: Исправил void на Task, добавил var - писал пост по памяти, был не прав. И насчёт того, что самая быстрая проверка - это через items.Count != 0 - совершенно верно было написано в исчезнувшем ответе, просто мне сама форма такой проверки кажется не такой красивой, как items.Any(). Но для оптимизации, видимо, придётся сделать именно так, если решу всё же поставить проверки. Тем более, что я проверил код - для ICollection всё-равно придётся проверять именно через Count, нет у него Any().
Ответы (2 шт):
Полагаю, только профилирование кода может дать точный ответ.
Всё остальное - гадание на картах.
Вообще, добавление такой проверки может сломать код.
if(items.Any())
{
filteredItems = await SomeAsyncFunction(items);
}
Если коллекция items пустая, то SomeAsyncFunction не будет вызвана. Но, возможно, она выполняла какое-то важное действие даже при наличии пустой коллекции. Побочный эффект это плохо, но всё же...
А если мы знаем внутреннее устройство SomeAsyncFunction и знаем, что её можно безопасно исключить, то это идёт вразрез с хорошими практиками программирования. С объектами нужно обращаться как с чёрными ящиками. Мы не обязаны знать их устройство.
Хотя, конечно, когда речь идёт о высоконагруженном коде, то хорошие практики и паттерны уходят на второй план.
В итоге сам провёл примитивный бенчмарк, и получилось в общем как и ожидалось - эффект есть, но заметен он только на большом количестве выполнений такого кода.
Проверял, выполняя параллельно через Task.WhenAll 1,000,000 (миллион) Task-ов с цепочкой async/await, практически ничего не делающих, а просто передающих друг-другу управление (как это и будет в случае пустой коллекции).
Время выполнения в случае всегда пустой коллекции:
- c проверкой пустая ли коллекция (и дальше код не выполняем): 2 секунды.
- без проверки на размер коллекции: 2.5 секунды.
- без проверки на размер и с добавленным foreach (по пустой коллекции!) внутри методов: 2.7 секунды.
Ну то есть некоторый эффект всё же есть, но заметен он при выполнении очень большого количества задач и на сугубо всегда пустой коллекции.
Далее немного усложнил условия. Теперь в 50% случаев коллекция пустая, а в 50% там 100 элементов и они там внутри функций просто добавляются через foreach в новый список и возвращаются.
Естественно, время выполнения теста в целом возросло и эффект ещё более размазался:
- с проверкой на пустоту коллекции: 5 секунд
- без проверки: 5.2 секунды
В общем, некоторой экономии ресурсов таким образом можно достигнуть, но всё будет зависеть от соотношения количества пустых/не пустых коллекций на входе и от того, насколько тяжёлые вычисления выполняются с коллекциями внутри функций. Сильно много таким образом явно не удастся сэкономить.