.NET Разработчик
6.51K subscribers
424 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День четыреста восемьдесят пятый. #PerformanceTips
Лучшие Практики по Производительности в C#. Начало
Недавно нашёл статью со списком «шаблонов кода, которых следует избегать, потому что они плохо работают в смысле производительности». Автор пишет, что все пункты в списке так или иначе вызывали проблемы с производительностью. Хотя, стоит оговориться, что не все советы могут работать во всех случаях.

1. Синхронное ожидание асинхронного кода
Не ожидайте синхронно незавершённых задач. Включая, но не ограничиваясь: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll. Более общий совет: любая синхронная зависимость между двумя потоками пула может вызвать истощение пула потоков.

2. ConfigureAwait
Если ваш код может быть исполнен без захвата контекста синхронизации, используйте ConfigureAwait(false) для каждого вызова await. Однако обратите внимание, что ConfigureAwait имеет смысл только при использовании ключевого слова await. Например, этот код не имеет смысла:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
Подробнее о ConfigureAwait в серии постов с тегом #AsyncAwaitFAQ

3. async void
Никогда не используйте async void. Исключение, выброшенное в таком методе, распространяется в контекст синхронизации и обычно приводит к сбою всего приложения. Если вы не можете вернуть задачу в свой метод (например, потому что вы реализуете интерфейс), переместите асинхронный код метод-обёртку и вызовите его:
interface IInterface {
void DoSomething();
}
class Implementation : IInterface {
public void DoSomething() {
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync() {
await Task.Delay(100);
}
}

4. По возможности избегайте слова async
По привычке вы можете написать:
public async Task CallAsync() {
var client = new Client();
return await client.GetAsync();
}
Хотя код семантически корректен, использование ключевого слова async здесь не требуется и может привести к значительным накладным расходам в «горячих путях». Попробуйте удалить его, если это возможно:
public Task CallAsync() {
var client = new Client();
return client.GetAsync();
}
Однако имейте в виду, что вы не можете использовать эту оптимизацию, когда ваш код упакован в блоки (например, try/catch или using):
public async Task Correct() {
using (var client = new Client()) {
return await client.GetAsync();
}
}
public Task Incorrect() {
using (var client = new Client()) {
return client.GetAsync();
}
}
В методе Incorrect, поскольку задача не ожидается внутри блока using, клиент может быть удален до завершения вызова GetAsync.

Продолжение следует…

Источник:
https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День четыреста восемьдесят шестой. #PerformanceTips
Лучшие Практики по Производительности в C#. Продолжение
5. Сравнения Строк Чувствительные к Культуре
Если у вас нет причин использовать чувствительные к культуре сравнения строк, всегда используйте нечувствительные сравнения (с параметром StringComparison.Ordinal). Хотя это и не имеет большого значения для латиницы из-за внутренних оптимизаций, сравнение происходит на порядок медленнее для других культур (до 2 порядков в Linux). Поскольку сравнение строк является частой операцией в большинстве приложений, такие мелкие задержки быстро накапливаются.

6. ConcurrentBag<T>
Не используйте ConcurrentBag<T> без тестирования производительности. Эта коллекция была разработана для очень специфических случаев использования (когда большую часть времени элемент извлекается из контейнера добавившим его потоком) и страдает от проблем с производительностью, если используется иначе. Если вам нужна конкурентная коллекция, рассмотрите ConcurrentQueue<T>. Подробнее о потокобезопасных коллекциях.

7. ReaderWriterLock<T>/ReaderWriterLockSlim<T>
Не используйте ReaderWriterLock<T>/ReaderWriterLockSlim<T> без тестирования производительности. Стоимость их использования намного выше, чем у простого монитора (используемого с ключевым словом lock). Если количество читателей критического блока не очень велико, уровня параллелизма будет недостаточно для амортизации возросших издержек, и код будет работать хуже.

8. Используйте Лямбда-Выражения Вместо Чистого Предиката
Рассмотрим следующий код:
private static bool Filter(int i) {
return i % 2 == 0;
}
И два варианта вызова:
list.Where(i => Filter(i));
и
list.Where(Filter);
Второй вариант приводит к выделению памяти в куче при каждом вызове, компилируясь в следующую конструкцию:
list.Where(new Func<int,bool>(Filter));
Лямбда-выражение использует оптимизацию компилятора и кэширует делегат в статическое поле.
*Примечание: я быстренько протестировал оба варианта в консольном приложении на предмет быстродействия и использования памяти и не нашёл никаких отличий ни в Framework, ни в Core. Поэтому есть подозрение, что оптимизация происходит в любом случае, а этот совет либо устарел, либо не проверялся автором.

Продолжение следует…

Источник:
https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День четыреста восемьдесят седьмой. #PerformanceTips
Лучшие Практики по Производительности в C#. Продолжение
9. Преобразование Перечислений в Строку
Вызов Enum.ToString в .Net является дорогостоящим, поскольку для преобразования используется рефлексия, а вызов виртуального метода структуры приводит к боксингу. Этого следует по возможности избегать. Часто перечисления могут быть заменены константами:
public enum Numbers {
One, Two, …
}
public static class Numbers {
public const string One = "One";
public const string Two = "Two";

}
В обоих случаях можно использовать Numbers.One, Numbers.Two,…

10. Сравнения Перечислений
Примечание: это поведение оптимизировано, начиная с .Net Core 2.1
При использовании перечислений в качестве флагов может возникнуть соблазн использовать метод Enum.HasFlag:
[Flags]
public enum Options {
Opt1 = 1, Opt2 = 2, Opt3 = 4
}

private Options option;
public bool IsOpt2() => option.HasFlag(Options.Opt2);
Этот код приводит к боксингу для преобразования Options.Opt2 в Enum и для виртуального вызова HasFlag на структуре, делает код необоснованно дорогим. Вместо этого можно использовать бинарные операторы:
public bool IsOpt2() => (option & Options.Opt2 == Options.Opt2);
Подробнее про битовые флаги

11. Реализуйте Проверку на Равенство для Структур
При использовании struct в сравнениях (например, при использовании в качестве ключа для словаря) необходимо переопределить методы Equals и GetHashCode. Реализация по умолчанию использует рефлексию и очень медленная. Подробнее

12. Избегайте Ненужного Боксинга при Использовании Структур с Интерфейсами
Рассмотрим следующий код:
public class IntValue : IValue {}

public void SendValue(IValue value) {…}
public void LogValue(IValue value) {…}

public void DoStuff() {
var value = new IntValue();
LogValue(value);
SendValue(value);
}
Соблазнительно сделать IntValue структурой, чтобы избежать выделения памяти в куче. Но поскольку AddValue и SendValue принимают интерфейс, а интерфейсы имеют ссылочную семантику, значение будет упаковываться при каждом вызове, сводя на нет преимущества «оптимизации». На самом деле, производительность может быть даже хуже, чем если бы IntValue был классом. Если же вы создаёте API, которому может быть передана структура, попробуйте использовать обобщённые методы:
public void SendValue<T>(T value) where T : IValue {…}
public void LogValue<T>(T value) where T : IValue {…}
Хотя на первый взгляд создание таких методов выглядит бесполезным, на самом деле это позволяет избежать боксинга, когда IntValue является структурой.

Продолжение следует…

Источник:
https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День четыреста восемьдесят восьмой. #PerformanceTips
Лучшие Практики по Производительности в C#. Окончание
13. Код Подписчиков CancellationToken Всегда Встраивается
Когда вы отменяете задачу через CancellationTokenSource, код всех подписчиков будет выполняться в текущем потоке. Это может привести к незапланированным паузам или даже неочевидным взаимным блокировкам:
var cts = new CancellationTokenSource ();
cts.Token.Register (() => Thread.Sleep (5000));
cts.Cancel (); // Это вызов заблокируется на 5 секунд
Вы не можете отказаться от этого поведения. Поэтому, при отмене через CancellationTokenSource, спросите себя, можете ли вы позволить текущему потоку исполнять другой код. Если нет, оберните вызов Cancel в Task.Run, чтобы выполнить его в пуле потоков. Подробнее о CancellationToken.

14. Код Продолжений TaskCompletionSource Часто Встраивается
Код продолжений TaskCompletionSource также часто встраивается. Это хорошая оптимизация, но она может быть причиной неочевидных ошибок. Их можно избежать, передав параметру TaskCompletionSource параметр TaskCreationOptions.RunContinuationsAsynchronously.
Если у вас нет веских причин этого не делать, всегда используйте этот параметр при создании TaskCompletionSource.
Внимание: код также скомпилируется, если вы используете TaskContinuationOptions.RunContinuationsAsynchronously. Но этот параметр будет проигнорирован, и продолжения будут оставаться встроенными. Это удивительно распространенная ошибка, потому что TaskContinuationOptions предшествует TaskCreationOptions при автозаполнении.

15. Task.Run / Task.Factory.StartNew
Если у вас нет причин использовать Task.Factory.StartNew, отдавайте предпочтение Task.Run для запуска фоновой задачи. Task.Run использует более безопасные значения по умолчанию, и, что более важно автоматически разворачивает возвращаемую задачу, что может предотвратить ошибки в асинхронных методах:
class Program {
public static async Task ProcessAsync() {
await Task.Delay(2000);
Console.WriteLine("Processing done");
}
static async Task Main(string[] args) {
await Task.Factory.StartNew(ProcessAsync);
Console.WriteLine("End of program");
Console.ReadLine();
}
}
Из кода это не очевидно, но "End of program" будет выведено раньше, чем "Processing done", потому что Task.Factory.StartNew вернёт Task<Task>, а код ожидает завершения только внешней задачи. Исправить это можно, используя либо
await Task.Factory.StartNew(ProcessAsync).Unwrap();
либо
await Task.Run(ProcessAsync);
Task.Factory.StartNew лучше использовать в следующих случаях:
- Запуск задачи в другом планировщике
- Выполнение задачи в выделенном потоке (с помощью TaskCreationOptions.LongRunning)
- Постановка задачи в глобальную очередь пула потоков (с помощью TaskCreationOptions.PreferFairness)

Источник: https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День восемьсот сороковой. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно
Разработка программного обеспечения - это поиск компромиссов:
- нормализация против денормализации в реляционных базах данных,
- скорость разработки против качественного кода
и т.п.

Высокопроизводительный код C# обычно требует компромиссов. Разработчики могут пожертвовать удобством сопровождения или безопасностью кода, чтобы код работал быстрее. Но это применимо только к сценариям, в которых уже применяются все шаблоны производительности и лучшие практики, а производительность всё равно требует дальнейшего улучшения.
Существует множество подходов, которые могут помочь разработчикам значительно улучшить производительность приложений, ничем не жертвуя.
Начнём с очевидного:

1. Указывайте ёмкость коллекции
Рассмотрим два почти идентичных метода:
public void NonFixedCapacityTest()
{
var items = new List<decimal>();
for (int i = 0; i < 1000000; i++)
items.Add(i);
}

public void FixedCapacityTest()
{
const int capacity = 1000000;
var items = new List<decimal>(capacity);
for (int i = 0; i < capacity; i++)
items.Add(i);
}

Оба метода выполняют одну и ту же задачу - заполнение коллекции целыми числами с помощью цикла foreach. Единственное отличие состоит в том, что в методе FixedCapacityTest конструктор коллекции инициализируется некоторым числом. Этот простой трюк заставляет метод FixedCapacityTest работать в два раза быстрее, чем NonFixedCapacityTest.

|               Method |      Mean |
|--------------------- |----------:|
| NonFixedCapacityTest | 22.708 ms |
| FixedCapacityTest | 8.418 ms |
Производительность в два-три раза выше, потому что List<T> реализован таким образом, что хранит элементы в массиве, который представляет собой структуру данных фиксированного размера. Когда разработчик создает экземпляр List<T> без указания его ёмкости, выделяется массив ёмкости по умолчанию. Когда массив заполнен, выделяется новый массив большего размера, а значения из старого массива копируются в новый.
Предварительное указание ёмкости устраняет накладные расходы на выделение, копирование и сборку мусора использованных массивов. Разработчики должны всегда указывать ёмкость коллекции, если они заранее знают, сколько элементов будет в неё добавлено.
Параметр ёмкости работает не только с коллекцией List, но и с другими, такими как Dictionary<TKey, TValue>, HashSet<T> и т.п.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок первый. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно

2. Используйте структуры вместо классов в некоторых случаях
Разработчикам часто может потребоваться выделить массив или список для хранения десятков тысяч объектов в памяти. Эту задачу можно решить с помощью класса или структуры.
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
public struct PointStruct
{
public int X { get; set; }
public int Y { get; set; }
}

public void ListOfObjectsTest()
{
const int length = 1000000;
var items = new List<PointClass>(length);
for (int i = 0; i < length; i++)
items.Add(new PointClass() { X = i, Y = i });
}

public void ListOfStructsTest()
{
const int length = 1000000;
var items = new List<PointStruct>(length);
for (int i = 0; i < length; i++)
items.Add(new PointStruct() { X = i, Y = i});
}

Как видите, единственная разница между ListOfObjectTest и ListOfStructsTest заключается в том, что первый создаёт экземпляры класса, а второй - экземпляры структур. Код PointClass идентичен коду PointStruct.

|            Method |      Mean |
|------------------ |----------:|
| ListOfObjectsTest | 67.724 ms |
| ListOfStructsTest | 5.136 ms |
Код, использующий структуры, работает в 10-15 раз быстрее, чем код, использующий классы. Такая большая разница во времени, объясняется тем, что в случае классов CLR должна выделить один миллион объектов в управляемой куче и сохранить ссылки на них в коллекции List<T>. В случае структур единственным объектом, размещённым в куче, будет экземпляр коллекции List<T>. Миллион структур будет встроен в этот единственный экземпляр коллекции.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок второй. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно
3. Распараллеливание циклов
Часто бывает необходимо перебрать коллекцию с помощью цикла foreach и выполнить некоторую логику для каждого элемента.
public void ForeachTest()
{
var items = Enumerable.Range(0, 100).ToList();
foreach (var item in items)
{
//Симулируем длинную операцию
Thread.Sleep(1);
}
}

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

Производительность можно повысить, начав использовать параллельную версию цикла foreach, которую платформа предоставляет разработчикам.
public void ParallelForeachTest()
{
var items = Enumerable.Range(0, 100).ToList();

Parallel.ForEach(items, (item) =>
{
//Симулируем длинную операцию
Thread.Sleep(1);
});
}

Parallel.Foreach можно использовать на любой коллекции, которая реализует IEnumerable<T> как обычный цикл foreach. Реализация Parallel.Foreach выполнит всю работу по распараллеливанию за вас:
- разобьёт коллекцию на части,
- назначит и выполнит эти части в отдельных потоках.

|              Method |       Mean |
|-------------------- |-----------:|
| ForeachTest | 1,543.9 ms |
| ParallelForeachTest | 199.9 ms |
Надо отметить, что если коллекции небольшие и время выполнения одной итерации быстрое, переход с foreach на Parallel.Foreach может даже ухудшить производительность, особенно если используется синхронизация потоков из-за доступа к общим ресурсам.

Функциональные возможности, предоставляемые классом Parallel, являются отличным выбором для длительных итераций, которые не зависят друг от друга и не требуют использования примитивов синхронизации, таких как потокобезопасные коллекции.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок третий. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно

4. Избегайте неявного линейного поиска
Линейный поиск - это один из простейших алгоритмов поиска, который перебирает все элементы коллекции один за другим, пока не будет найден указанный элемент.

Хотя разработчики обычно не реализуют алгоритм поиска явно, линейный поиск всё же часто вызывает снижение производительности.
public void LinearSearchTest()
{
var ids = Enumerable.Range(0, 10000000);
int idToFind = 9193513;
var exists = ids.Any(u => u == idToFind);
}

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

Разработчикам не обязательно знать, как реализован каждый из методов LINQ. Важно знать основы: в .NET коллекция List<T> базируется на массиве. А когда дело доходит до поиска значения в несортированном массиве, его сложность составляет O(n). Независимо от того, какой метод LINQ используется для поиска значения в массиве (Any, Contains или Where), сложность остается прежней.

Решением этой конкретной проблемы было бы использование структуры данных, подходящей для конкретной задачи. В нашем случае у нас есть идентификаторы, которые всегда уникальны. Это позволяет нам преобразовать коллекцию в HashSet<T>.
public void HashSetTest()
{
var ids = Enumerable.Range(0, 10000000).ToHashSet();
int idToFind = 9193513;
var exists = ids.Contains(idToFind);
}

| Method | Mean |
|----------------- |----------:|
| LinearSearchTest | 63.74 ms |
| HashSetTest | 334.34 ms |
Погодите-ка. HashSetTest оказался гораздо медленнее, чем LinearSearchTest. Это связано с тем, что время тратится и на создание коллекции HashSet<T>, что является трудоёмкой операцией для больших наборов.

Разработчики выиграют от преобразования в HashSet только в том случае, если они планируют часто вызывать на коллекции метод Contains. Если вынести создание коллекции из тестов производительности и измерить только время нахождения элемента, результаты будут кардинально отличаться. Поиск значения в HashSet<T> почти не занимает времени по сравнению с поиском значения в коллекции List<T>.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок четвёртый. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно

5. Материализуйте запросы LINQ один раз
При написании запросов LINQ с использованием интерфейсов IEnumerable или IQueryable разработчики могут материализовать запрос (вызвать ToList, ToArray или аналогичные методы) или не делать этого, что позволяет лениво работать с коллекциями. Но иногда возникает необходимость перебрать одну и ту же коллекцию несколько раз. Если запрос не был материализован, повторный перебор коллекции повлияет на производительность.
public void NotMaterializedQueryTest()
{
var elements = Enumerable.Range(0, 50000000);
var filtered =
elements.Where(e => e % 100000 == 0);

foreach (var e in filtered)
{

}

foreach (var e in filtered)
{

}

foreach (var e in filtered)
{

}
}

В этом примере запрос Where не материализуется. Вызов метода Where просто возвращает объект, реализующий интерфейс IEnumerable. Методы GetEnumerator и MoveNext будут вызываться только при итерации по коллекции в цикле foreach.

Вот пример с материализованным запросом:
public void MaterializedQueryTest()
{
var elements = Enumerable.Range(0, 50000000);
var filtered =
elements.Where(e => e % 100000 == 0).ToList();

//остальной код такой же
}

Второй метод из-за материализации запроса с помощью ToList будет работать в 3 раза быстрее первого.
|                   Method |       Mean |
|------------------------- |-----------:|
| NotMaterializedQueryTest | 1,299.6 ms |
| MaterializedQueryTest | 495.5 ms |

Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День 1593. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
I. Управление Памятью и Сборка Мусора
Управление памятью и сборка мусора являются важными аспектами настройки производительности в C#, поэтому эти рекомендации помогут вам оптимизировать код для достижения максимальной эффективности.

1. Используйте интерфейс IDisposable
IDisposable помогает правильно управлять неуправляемыми ресурсами и обеспечивает эффективное использование памяти вашим приложением.
Плохо:
public class ResourceHolder
{
private Stream _stream;

public ResourceHolder(string filePath)
{
_stream = File.OpenRead(filePath);
}
//…
}

ResourceHolder не реализует интерфейс IDisposable, поэтому неуправляемые ресурсы могут не освобождаться, что приводит к утечке памяти.
Хорошо:
public class ResourceHolder : IDisposable
{
private Stream _stream;

public ResourceHolder(string filePath)
{
_stream = File.OpenRead(filePath);
}

public void Dispose()
{
_stream?.Dispose();
}
}

Реализуя интерфейс IDisposable, вы гарантируете, что неуправляемые ресурсы будут освобождены, когда они больше не нужны, что предотвратит утечку памяти и уменьшит нагрузку на сборщик мусора.

Хотя, с IDisposable не всегда бывает так просто. Подробнее об этом можно почитать в серии постов Мои любимые ошибки с IDisposable:
- 1
- 2
- 3

2. Избегайте преждевременной оптимизации
Преждевременная оптимизация может привести к обратным результатам, усложняя чтение, поддержку и расширение кода.

Слишком большое внимание микрооптимизациям может привести к сложному, загромождённому коду, который жертвует простотой поддержки ради незначительного повышения производительности. Преждевременные оптимизации могут усложнить поддержку кода, при этом не оказав существенного влияния на общую производительность или даже снизив производительность. Далеко не всё, что интуитивно кажется должно ускорить выполнение кода, в реальности его ускоряет.

Важно сначала сосредоточиться на написании чистого, эффективного кода и оптимизировать его только при необходимости после тщательного профилирования приложения. Такой подход приведет к созданию более удобных в сопровождении и более производительных приложений.

См. подробнее про оптимизацию кода.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍15
День 1594. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
II. Асинхронное программирование и async/await
Асинхронное программирование — мощный метод повышения производительности в операциях, связанных с вводом-выводом, позволяющий повысить скорость отклика и эффективность приложения.

1. Ограничьте количество одновременных операций
Управление параллелизмом имеет решающее значение для оптимизации производительности. Ограничивая количество одновременных операций в приложении, вы помогаете снизить общую нагрузку на систему.
Плохо:
public async Task ProcessManyItems(List<string> items)
{
var tasks = items.Select(
async item => await ProcessItem(item));
await Task.WhenAll(tasks);
}

Здесь задачи создаются одновременно для каждого элемента без надлежащего ограничения, что может вызвать значительную нагрузку на систему.
Хорошо:
public async Task ProcessManyItems(
List<string> items,
int maxConcurrency = 10)
{
using (var semaphore = new
SemaphoreSlim(maxConcurrency))
{
var tasks = items.Select(async item =>
{
// Ограничиваем конкурентность семафором
await semaphore.WaitAsync();
try
{
await ProcessItem(item);
}
finally
{
semaphore.Release();
}
});

await Task.WhenAll(tasks);
}
}
Без ограничения параллелизма множество задач будут выполняться одновременно, что может привести к большой нагрузке и снижению общей производительности. Вместо этого используйте SemaphoreSlim для управления количеством одновременных операций. Это отличный пример того, как повысить производительность приложения без ущерба для удобства чтения или сопровождения.

2. Используйте UseConfigureAwait(false), где возможно
Плохо:
public async Task<string> LoadDataAsync()
{
var data = await ReadDataAsync();
return ProcessData(data);
}

ConfigureAwait(false) — ценный приём, который может помочь предотвратить взаимоблокировки в асинхронном коде и повысить эффективность, не заставляя продолжения выполняться в исходном контексте синхронизации.
Хорошо:
public async Task<string> LoadDataAsync()
{
var data = await ReadDataAsync()
.ConfigureAwait(false);
return ProcessData(data);
}

Используйте ConfigureAwait(false) всегда в библиотечном коде и приложениях, не связанных с пользовательским интерфейсом.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍14👎6
День 1595. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
III. Параллельные вычисления и библиотека параллельных задач (TPL)
Параллельные вычисления могут помочь использовать мощность многоядерных процессоров и ускорить операции, связанные с ЦП.

1. Используйте параллельные циклы с Parallel.For() и Parallel.ForEach()
Плохо:
private void ProcessData(List<int> data)
{
for (int i = 0; i < data.Count; i++)
{
PerformExpensiveOperation(data[i]);
}
}

Здесь для обработки данных используется стандартный цикл for, что приводит к последовательному выполнению операций. Это не позволяет использовать весь потенциал современных многоядерных процессоров.
Хорошо:
private void ProcessData(List<int> data)
{
Parallel.ForEach(
data, item =>
PerformExpensiveOperation(item));
}

Параллельные циклы могут значительно ускорить обработку больших коллекций за счет распределения нагрузки между несколькими ядрами ЦП. Переключайтесь с обычных циклов for и foreach на их параллельные аналоги всякий раз, когда это возможно и безопасно.

2. Используйте класс Partitioner для эффективного распределения рабочей нагрузки.
Плохо:
private void ProcessData(IEnumerable<int> data)
{
Parallel.ForEach(data, item =>
PerformExpensiveOperation(item));
}

Здесь не уделяется особого внимания оптимизации разделения рабочей нагрузки между параллельными задачами. Это может привести к потенциальным накладным расходам и неравномерному распределению нагрузки.
Хорошо:
private void ProcessData(IEnumerable<int> data)
{
var partitioner = Partitioner.Create(data);
Parallel.ForEach(partitioner, item =>
PerformExpensiveOperation(item));
}

Используя класс Partitioner, вы можете эффективно распределять рабочие нагрузки по частям, уменьшая потенциальные накладные расходы и улучшая балансировку нагрузки между параллельными задачами.

Partitioner создает оптимальные рабочие блоки, чтобы свести к минимуму накладные расходы на синхронизацию задач, что приводит к повышению производительности и распределению рабочей нагрузки для ваших приложений.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍21
День 1596. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
IV. Важность кэширования данных
Кэширование может значительно повысить производительность приложений за счёт сокращения времени, затрачиваемого на выборку и обработку данных.

1. Реализовать кэширование данных с помощью кэша в памяти
Использование кэширования в памяти может значительно сократить время, требующее выборки данных из базы данных, и ускорить приложение.
Плохо:
public Product GetProductById(int id)
{
// Извлечение данных из БД каждый раз
var pr = _dbContext.Products
.FirstOrDefault(p => p.Id == id);
return pr;
}

Здесь данные о продукте извлекаются из базы каждый раз, когда вызывается метод. Это может привести к значительному снижению производительности, особенно если база данных расположена удалённо или находится под большой нагрузкой.
Хорошо:
private static MemoryCache _cache = 
new MemoryCache(new MemoryCacheOptions());

public Product GetProductById(int id)
{
// Извлечение из кэша, если возможно
if (!_cache.TryGetValue(id, out Product pr))
{
pr = _dbContext.Products
.FirstOrDefault(p => p.Id == id);
_cache.Set(id, pr, TimeSpan.FromMinutes(30));
}

return pr;
}

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

2. Реализуйте кэширование с помощью распределённых систем кэширования
Распределённые системы кэширования, такие как Redis, могут ещё больше повысить производительность вашего приложения за счёт кэширования данных способом, который масштабируется на нескольких серверах и обеспечивает высокую доступность. Например, используем распределённый кэш для извлечения списка популярных продуктов:
private static IDistributedCache _dCache;

public List<Product> GetProducts()
{
var key = "popularProducts";
var cached = _dCache.GetString(key);
if (cached == null)
{
var pr = _dbContext.Products
.Where(p => p.IsPopular).ToList();
_dCache.SetString(key,
JsonConvert.SerializeObject(pr),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(30)
});

return pr;
}
else
{
return JsonConvert
.DeserializeObject<List<Product>>(cached);
}
}

Здесь мы используем распределённое кэширование с помощью Redis для хранения данных о популярных продуктах, что сокращает частоту выборки из базы данных. Используйте распределённые системы кэширования для кэширования на нескольких серверах и улучшения масштабируемости приложений.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍13
День 1597. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
V. Параллелизм и безопасность потоков
Обеспечение потокобезопасности может предотвратить нежелательные ошибки и проблемы с производительностью.

1. По возможности используйте структуры данных без блокировок
Выбор структур данных, таких как ConcurrentBag, ConcurrentQueue или ConcurrentDictionary, может помочь вам обеспечить безопасность потоков в многопоточных сценариях без ущерба для производительности.
Плохо:
private object _sync = new object();
private List<int> _list = new List<int>();

public void Add(int item)
{
lock (_sync)
{
_list.Add(item);
}
}

Здесь используется блокировка для синхронизации доступа к списку, что может привести к конфликтам и снижению производительности.
Хорошо:
private ConcurrentBag<int> _bag =
new ConcurrentBag<int>();
public void Add(int item)
{
_bag.Add(item);
}

Используя структуры данных без блокировок, вы можете свести к минимуму конфликты, повысить производительность и обеспечить потокобезопасность в многопоточных сценариях.

2. Используйте эффективные конструкции синхронизации
Использование SemaphoreSlim, ReaderWriterLockSlim или Monitor, может помочь вам защитить общие ресурсы и обеспечить безопасность потоков, сводя к минимуму конфликты и влияние на производительность.
Плохо: см. выше.

Хорошо:
private SemaphoreSlim _semaphore
= new SemaphoreSlim(1, 1);
private List<int> _list = new List<int>();

public async Task AddAsync(int item)
{
await _semaphore.WaitAsync();
try
{
_list.Add(item);
}
finally
{
_semaphore.Release();
}
}

Эффективные конструкции синхронизации позволяют защитить общие ресурсы и обеспечить безопасность потоков, сводя к минимуму конфликты и влияние на производительность.

3. Используйте Interlocked для атомарных операций
Вы можете выполнять простые атомарные операции, не полагаясь на блокировки, уменьшая конкуренцию и повышая производительность.
Плохо:
private int _counter;
private object _sync = new object();

public void Increment()
{
lock (_syncRoot)
{
_counter++;
}
}

Здесь ключевое слово lock используется для обеспечения потокобезопасности для увеличения счетчика. Однако это может привести к конфликтам и снижению производительности.
Хорошо:
private int _counter;
public void Increment ()
{
Interlocked.Increment(ref _counter);
}

Класс Interlocked позволяет выполнять простые атомарные операции без использования блокировок, что повышает производительность и снижает количество конфликтов. Используйте его, когда это возможно, для таких операций, как инкремент, декремент или добавление.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍12
День 1604. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
VI. Оптимизация обработки исключений
Обработка исключений — важнейший аспект программирования, но неправильное использование может привести к снижению производительности. Посмотрим, как эффективно и ответственно обрабатывать исключения.

1. Избегайте использования исключений для управления потоком
Обработка исключений как части нормального потока исполнения программы может значительно повлиять на производительность, создавая ненужную работу для оптимизатора и приводя к потенциальным проблемам производительности во время выполнения.
Плохо:
try
{
int.Parse(input);
}
catch (FormatException)
{
// обработка неправильного ввода
}
Здесь попытка парсинга недопустимой входной строки вызовет исключение. Генерация исключения здесь не идеальна для производительности и вынуждает обрабатывать FormatException как часть потока исполнения программы.

Хорошо:
if (int.TryParse(input, out int result))
{
// Используем значение
}
else
{
// обработка неправильного ввода
}
Здесь используется метод TryParse, чтобы не полагаться на исключение для потока исполнения. Такой подход обеспечивает лучшую производительность и более чистый код.

2. Используйте фильтры исключений, чтобы свести к минимуму блоки захвата
Фильтры исключений помогают писать эффективный код обработки исключений, который делает блоки захвата более краткими и простыми в обслуживании.
Плохо:
try
{
// …
}
catch (Exception ex)
{
if (ex is InvalidOperationException
|| ex is ArgumentNullException)
{
// обработка этих видов исключений
}
else
{
throw;
}
}
Здесь несколько исключений перехватываются в одном блоке catch с вложенными операторами if, используемыми для определения типа их обработки. Это может привести к более запутанному и сложному в сопровождении коду.

Хорошо:
try
{
// …
}
catch (Exception ex) when (
ex is InvalidOperationException ||
ex is ArgumentNullException)
{
// обработка этих видов исключений
}
Хороший пример демонстрирует использование фильтров исключений. Это позволяет перехватывать исключения только при выполнении определённого условия, что упрощает блоки перехвата и устраняет необходимость в нескольких блоках перехвата или повторной генерации необработанных исключений. См. подробнее о фильтрах исключений

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍8
День 1609. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
VII. Обнуляемость и обнуляемые ссылочные типы
Обработка ссылочных типов, допускающих значение null, является важной частью программирования на C#, особенно для предотвращения исключений NullReferenceException. Рассмотрим несколько советов по безопасной работе с типами, допускающими значение null, без ущерба для производительности.

1. Используйте операторы объединения с null (??, ??=)
Операторы объединения с null помогают писать краткий и производительный код при работе с типами, допускающими значение NULL, гарантируя, что значения null заменяются значением по умолчанию.
Плохо:
string input = GetNullableString();
if (input == null)
{
input = "default";
}
Неудачный пример демонстрирует многословный и менее производительный код при работе с нулевыми значениями.

Хорошо:
var input = GetNullableString() ?? "default";
Здесь используется оператор объединения с нулевым значением, который обеспечивает более лаконичный и эффективный способ обработки нулевых значений в C#. Это обеспечивает лучшую производительность и более понятный код.

2. Используйте обнуляемые ссылочные типы, чтобы избежать исключений NullReferenceException во время выполнения
Обнуляемые ссылочные типы, появившиеся в C# 8.0, помогают перехватывать потенциальные исключения NullReferenceException во время компиляции, а не во время выполнения.
Плохо:
string name = GetName();
int length = name.Length;
Здесь у нас потенциально во время выполнения может возникнуть исключение NullReferenceException, что может привести к неожиданным сбоям.

Хорошо:
string? name = GetName();
int length = name?.Length ?? 0;
Используя обнуляемые ссылочные типы, и условный доступ с нулевым значением через оператор ?., вы можете избежать потенциальных исключений NullReferenceException в своём коде. Это помогает создавать более безопасный и производительный код, который легче понять как во время разработки, так и во время отладки.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍13