День четыреста восемьдесят пятый. #PerformanceTips
Лучшие Практики по Производительности в C#. Начало
Недавно нашёл статью со списком «шаблонов кода, которых следует избегать, потому что они плохо работают в смысле производительности». Автор пишет, что все пункты в списке так или иначе вызывали проблемы с производительностью. Хотя, стоит оговориться, что не все советы могут работать во всех случаях.
1. Синхронное ожидание асинхронного кода
Не ожидайте синхронно незавершённых задач. Включая, но не ограничиваясь:
2. ConfigureAwait
Если ваш код может быть исполнен без захвата контекста синхронизации, используйте
3. async void
Никогда не используйте
По привычке вы можете написать:
Продолжение следует…
Источник: https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
Лучшие Практики по Производительности в 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
в серии постов с тегом #AsyncAwaitFAQ3. async void
Никогда не используйте
async void
. Исключение, выброшенное в таком методе, распространяется в контекст синхронизации и обычно приводит к сбою всего приложения. Если вы не можете вернуть задачу в свой метод (например, потому что вы реализуете интерфейс), переместите асинхронный код метод-обёртку и вызовите его:interface IInterface {4. По возможности избегайте слова async
void DoSomething();
}
class Implementation : IInterface {
public void DoSomething() {
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync() {
await Task.Delay(100);
}
}
По привычке вы можете написать:
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. Сравнения Строк Чувствительные к Культуре
Если у вас нет причин использовать чувствительные к культуре сравнения строк, всегда используйте нечувствительные сравнения (с параметром
6. ConcurrentBag<T>
Не используйте
7. ReaderWriterLock<T>/ReaderWriterLockSlim<T>
Не используйте
8. Используйте Лямбда-Выражения Вместо Чистого Предиката
Рассмотрим следующий код:
*Примечание: я быстренько протестировал оба варианта в консольном приложении на предмет быстродействия и использования памяти и не нашёл никаких отличий ни в Framework, ни в Core. Поэтому есть подозрение, что оптимизация происходит в любом случае, а этот совет либо устарел, либо не проверялся автором.
Продолжение следует…
Источник: https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
Лучшие Практики по Производительности в 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. Преобразование Перечислений в Строку
Вызов
10. Сравнения Перечислений
Примечание: это поведение оптимизировано, начиная с .Net Core 2.1
При использовании перечислений в качестве флагов может возникнуть соблазн использовать метод
11. Реализуйте Проверку на Равенство для Структур
При использовании
12. Избегайте Ненужного Боксинга при Использовании Структур с Интерфейсами
Рассмотрим следующий код:
Продолжение следует…
Источник: https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
Лучшие Практики по Производительности в 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 Всегда Встраивается
Когда вы отменяете задачу через
14. Код Продолжений TaskCompletionSource Часто Встраивается
Код продолжений
Если у вас нет веских причин этого не делать, всегда используйте этот параметр при создании
Внимание: код также скомпилируется, если вы используете
15. Task.Run / Task.Factory.StartNew
Если у вас нет причин использовать
- Запуск задачи в другом планировщике
- Выполнение задачи в выделенном потоке (с помощью
- Постановка задачи в глобальную очередь пула потоков (с помощью
Источник: https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
Лучшие Практики по Производительности в 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. Указывайте ёмкость коллекции
Рассмотрим два почти идентичных метода:
Предварительное указание ёмкости устраняет накладные расходы на выделение, копирование и сборку мусора использованных массивов. Разработчики должны всегда указывать ёмкость коллекции, если они заранее знают, сколько элементов будет в неё добавлено.
Параметр ёмкости работает не только с коллекцией
Продолжение следует…
Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
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. Используйте структуры вместо классов в некоторых случаях
Разработчикам часто может потребоваться выделить массив или список для хранения десятков тысяч объектов в памяти. Эту задачу можно решить с помощью класса или структуры.
Продолжение следует…
Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
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 |Код, использующий структуры, работает в 10-15 раз быстрее, чем код, использующий классы. Такая большая разница во времени, объясняется тем, что в случае классов CLR должна выделить один миллион объектов в управляемой куче и сохранить ссылки на них в коллекции
|------------------ |----------:|
| ListOfObjectsTest | 67.724 ms |
| ListOfStructsTest | 5.136 ms |
List<T>
. В случае структур единственным объектом, размещённым в куче, будет экземпляр коллекции List<T>
. Миллион структур будет встроен в этот единственный экземпляр коллекции.Продолжение следует…
Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок второй. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно
3. Распараллеливание циклов
Часто бывает необходимо перебрать коллекцию с помощью цикла
Производительность можно повысить, начав использовать параллельную версию цикла
- разобьёт коллекцию на части,
- назначит и выполнит эти части в отдельных потоках.
Функциональные возможности, предоставляемые классом Parallel, являются отличным выбором для длительных итераций, которые не зависят друг от друга и не требуют использования примитивов синхронизации, таких как потокобезопасные коллекции.
Продолжение следует…
Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
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. Избегайте неявного линейного поиска
Линейный поиск - это один из простейших алгоритмов поиска, который перебирает все элементы коллекции один за другим, пока не будет найден указанный элемент.
Хотя разработчики обычно не реализуют алгоритм поиска явно, линейный поиск всё же часто вызывает снижение производительности.
Разработчикам не обязательно знать, как реализован каждый из методов LINQ. Важно знать основы: в .NET коллекция
Решением этой конкретной проблемы было бы использование структуры данных, подходящей для конкретной задачи. В нашем случае у нас есть идентификаторы, которые всегда уникальны. Это позволяет нам преобразовать коллекцию в
Разработчики выиграют от преобразования в
Продолжение следует…
Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
5 Способов Повысить Производительность Кода C# Бесплатно
4. Избегайте неявного линейного поиска
Линейный поиск - это один из простейших алгоритмов поиска, который перебирает все элементы коллекции один за другим, пока не будет найден указанный элемент.
Хотя разработчики обычно не реализуют алгоритм поиска явно, линейный поиск всё же часто вызывает снижение производительности.
public void LinearSearchTest()В этом примере метод Any использует алгоритм линейного поиска, чтобы проверить, что указанный идентификатор находится в коллекции.
{
var ids = Enumerable.Range(0, 10000000);
int idToFind = 9193513;
var exists = ids.Any(u => u == idToFind);
}
Разработчикам не обязательно знать, как реализован каждый из методов 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 с использованием интерфейсов
Вот пример с материализованным запросом:
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 |Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
|------------------------- |-----------:|
| NotMaterializedQueryTest | 1,299.6 ms |
| MaterializedQueryTest | 495.5 ms |
День 1593. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
I. Управление Памятью и Сборка Мусора
Управление памятью и сборка мусора являются важными аспектами настройки производительности в C#, поэтому эти рекомендации помогут вам оптимизировать код для достижения максимальной эффективности.
1. Используйте интерфейс IDisposable
IDisposable помогает правильно управлять неуправляемыми ресурсами и обеспечивает эффективное использование памяти вашим приложением.
Плохо:
Хорошо:
Хотя, с IDisposable не всегда бывает так просто. Подробнее об этом можно почитать в серии постов Мои любимые ошибки с IDisposable:
- 1
- 2
- 3
2. Избегайте преждевременной оптимизации
Преждевременная оптимизация может привести к обратным результатам, усложняя чтение, поддержку и расширение кода.
Слишком большое внимание микрооптимизациям может привести к сложному, загромождённому коду, который жертвует простотой поддержки ради незначительного повышения производительности. Преждевременные оптимизации могут усложнить поддержку кода, при этом не оказав существенного влияния на общую производительность или даже снизив производительность. Далеко не всё, что интуитивно кажется должно ускорить выполнение кода, в реальности его ускоряет.
Важно сначала сосредоточиться на написании чистого, эффективного кода и оптимизировать его только при необходимости после тщательного профилирования приложения. Такой подход приведет к созданию более удобных в сопровождении и более производительных приложений.
См. подробнее про оптимизацию кода.
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
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. Ограничьте количество одновременных операций
Управление параллелизмом имеет решающее значение для оптимизации производительности. Ограничивая количество одновременных операций в приложении, вы помогаете снизить общую нагрузку на систему.
Плохо:
Хорошо:
2. Используйте UseConfigureAwait(false), где возможно
Плохо:
Хорошо:
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
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(Без ограничения параллелизма множество задач будут выполняться одновременно, что может привести к большой нагрузке и снижению общей производительности. Вместо этого используйте SemaphoreSlim для управления количеством одновременных операций. Это отличный пример того, как повысить производительность приложения без ущерба для удобства чтения или сопровождения.
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);
}
}
2. Используйте UseConfigureAwait(false), где возможно
Плохо:
public async Task<string> LoadDataAsync()ConfigureAwait(false) — ценный приём, который может помочь предотвратить взаимоблокировки в асинхронном коде и повысить эффективность, не заставляя продолжения выполняться в исходном контексте синхронизации.
{
var data = await ReadDataAsync();
return ProcessData(data);
}
Хорошо:
public async Task<string> LoadDataAsync()Используйте ConfigureAwait(false) всегда в библиотечном коде и приложениях, не связанных с пользовательским интерфейсом.
{
var data = await ReadDataAsync()
.ConfigureAwait(false);
return ProcessData(data);
}
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍14👎6
День 1595. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
III. Параллельные вычисления и библиотека параллельных задач (TPL)
Параллельные вычисления могут помочь использовать мощность многоядерных процессоров и ускорить операции, связанные с ЦП.
1. Используйте параллельные циклы с Parallel.For() и Parallel.ForEach()
Плохо:
Хорошо:
2. Используйте класс Partitioner для эффективного распределения рабочей нагрузки.
Плохо:
Хорошо:
Partitioner создает оптимальные рабочие блоки, чтобы свести к минимуму накладные расходы на синхронизацию задач, что приводит к повышению производительности и распределению рабочей нагрузки для ваших приложений.
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
III. Параллельные вычисления и библиотека параллельных задач (TPL)
Параллельные вычисления могут помочь использовать мощность многоядерных процессоров и ускорить операции, связанные с ЦП.
1. Используйте параллельные циклы с Parallel.For() и Parallel.ForEach()
Плохо:
private void ProcessData(List<int> data)Здесь для обработки данных используется стандартный цикл for, что приводит к последовательному выполнению операций. Это не позволяет использовать весь потенциал современных многоядерных процессоров.
{
for (int i = 0; i < data.Count; i++)
{
PerformExpensiveOperation(data[i]);
}
}
Хорошо:
private void ProcessData(List<int> data)Параллельные циклы могут значительно ускорить обработку больших коллекций за счет распределения нагрузки между несколькими ядрами ЦП. Переключайтесь с обычных циклов for и foreach на их параллельные аналоги всякий раз, когда это возможно и безопасно.
{
Parallel.ForEach(
data, item =>
PerformExpensiveOperation(item));
}
2. Используйте класс Partitioner для эффективного распределения рабочей нагрузки.
Плохо:
private void ProcessData(IEnumerable<int> data)Здесь не уделяется особого внимания оптимизации разделения рабочей нагрузки между параллельными задачами. Это может привести к потенциальным накладным расходам и неравномерному распределению нагрузки.
{
Parallel.ForEach(data, item =>
PerformExpensiveOperation(item));
}
Хорошо:
private void ProcessData(IEnumerable<int> data)Используя класс Partitioner, вы можете эффективно распределять рабочие нагрузки по частям, уменьшая потенциальные накладные расходы и улучшая балансировку нагрузки между параллельными задачами.
{
var partitioner = Partitioner.Create(data);
Parallel.ForEach(partitioner, item =>
PerformExpensiveOperation(item));
}
Partitioner создает оптимальные рабочие блоки, чтобы свести к минимуму накладные расходы на синхронизацию задач, что приводит к повышению производительности и распределению рабочей нагрузки для ваших приложений.
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍21
День 1596. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
IV. Важность кэширования данных
Кэширование может значительно повысить производительность приложений за счёт сокращения времени, затрачиваемого на выборку и обработку данных.
1. Реализовать кэширование данных с помощью кэша в памяти
Использование кэширования в памяти может значительно сократить время, требующее выборки данных из базы данных, и ускорить приложение.
Плохо:
Хорошо:
2. Реализуйте кэширование с помощью распределённых систем кэширования
Распределённые системы кэширования, такие как Redis, могут ещё больше повысить производительность вашего приложения за счёт кэширования данных способом, который масштабируется на нескольких серверах и обеспечивает высокую доступность. Например, используем распределённый кэш для извлечения списка популярных продуктов:
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
IV. Важность кэширования данных
Кэширование может значительно повысить производительность приложений за счёт сокращения времени, затрачиваемого на выборку и обработку данных.
1. Реализовать кэширование данных с помощью кэша в памяти
Использование кэширования в памяти может значительно сократить время, требующее выборки данных из базы данных, и ускорить приложение.
Плохо:
public Product GetProductById(int id)Здесь данные о продукте извлекаются из базы каждый раз, когда вызывается метод. Это может привести к значительному снижению производительности, особенно если база данных расположена удалённо или находится под большой нагрузкой.
{
// Извлечение данных из БД каждый раз
var pr = _dbContext.Products
.FirstOrDefault(p => p.Id == id);
return pr;
}
Хорошо:
private static MemoryCache _cache =Использование кэширования в памяти для хранения данных сокращает затраты на выборку базы. Используйте MemoryCache для кэширования часто запрашиваемых данных и повышения производительности.
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;
}
2. Реализуйте кэширование с помощью распределённых систем кэширования
Распределённые системы кэширования, такие как Redis, могут ещё больше повысить производительность вашего приложения за счёт кэширования данных способом, который масштабируется на нескольких серверах и обеспечивает высокую доступность. Например, используем распределённый кэш для извлечения списка популярных продуктов:
private static IDistributedCache _dCache;Здесь мы используем распределённое кэширование с помощью Redis для хранения данных о популярных продуктах, что сокращает частоту выборки из базы данных. Используйте распределённые системы кэширования для кэширования на нескольких серверах и улучшения масштабируемости приложений.
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);
}
}
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍13
День 1597. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
V. Параллелизм и безопасность потоков
Обеспечение потокобезопасности может предотвратить нежелательные ошибки и проблемы с производительностью.
1. По возможности используйте структуры данных без блокировок
Выбор структур данных, таких как ConcurrentBag, ConcurrentQueue или ConcurrentDictionary, может помочь вам обеспечить безопасность потоков в многопоточных сценариях без ущерба для производительности.
Плохо:
Хорошо:
2. Используйте эффективные конструкции синхронизации
Использование SemaphoreSlim, ReaderWriterLockSlim или Monitor, может помочь вам защитить общие ресурсы и обеспечить безопасность потоков, сводя к минимуму конфликты и влияние на производительность.
Плохо: см. выше.
Хорошо:
3. Используйте Interlocked для атомарных операций
Вы можете выполнять простые атомарные операции, не полагаясь на блокировки, уменьшая конкуренцию и повышая производительность.
Плохо:
Хорошо:
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
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. Избегайте использования исключений для управления потоком
Обработка исключений как части нормального потока исполнения программы может значительно повлиять на производительность, создавая ненужную работу для оптимизатора и приводя к потенциальным проблемам производительности во время выполнения.
Плохо:
Хорошо:
2. Используйте фильтры исключений, чтобы свести к минимуму блоки захвата
Фильтры исключений помогают писать эффективный код обработки исключений, который делает блоки захвата более краткими и простыми в обслуживании.
Плохо:
Хорошо:
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
VI. Оптимизация обработки исключений
Обработка исключений — важнейший аспект программирования, но неправильное использование может привести к снижению производительности. Посмотрим, как эффективно и ответственно обрабатывать исключения.
1. Избегайте использования исключений для управления потоком
Обработка исключений как части нормального потока исполнения программы может значительно повлиять на производительность, создавая ненужную работу для оптимизатора и приводя к потенциальным проблемам производительности во время выполнения.
Плохо:
tryЗдесь попытка парсинга недопустимой входной строки вызовет исключение. Генерация исключения здесь не идеальна для производительности и вынуждает обрабатывать FormatException как часть потока исполнения программы.
{
int.Parse(input);
}
catch (FormatException)
{
// обработка неправильного ввода
}
Хорошо:
if (int.TryParse(input, out int result))Здесь используется метод TryParse, чтобы не полагаться на исключение для потока исполнения. Такой подход обеспечивает лучшую производительность и более чистый код.
{
// Используем значение
}
else
{
// обработка неправильного ввода
}
2. Используйте фильтры исключений, чтобы свести к минимуму блоки захвата
Фильтры исключений помогают писать эффективный код обработки исключений, который делает блоки захвата более краткими и простыми в обслуживании.
Плохо:
tryЗдесь несколько исключений перехватываются в одном блоке catch с вложенными операторами if, используемыми для определения типа их обработки. Это может привести к более запутанному и сложному в сопровождении коду.
{
// …
}
catch (Exception ex)
{
if (ex is InvalidOperationException
|| ex is ArgumentNullException)
{
// обработка этих видов исключений
}
else
{
throw;
}
}
Хорошо:
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 заменяются значением по умолчанию.
Плохо:
Хорошо:
2. Используйте обнуляемые ссылочные типы, чтобы избежать исключений NullReferenceException во время выполнения
Обнуляемые ссылочные типы, появившиеся в C# 8.0, помогают перехватывать потенциальные исключения NullReferenceException во время компиляции, а не во время выполнения.
Плохо:
Хорошо:
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
Советы по Оптимизации Производительности
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();Здесь у нас потенциально во время выполнения может возникнуть исключение NullReferenceException, что может привести к неожиданным сбоям.
int length = name.Length;
Хорошо:
string? name = GetName();Используя обнуляемые ссылочные типы, и условный доступ с нулевым значением через оператор ?., вы можете избежать потенциальных исключений NullReferenceException в своём коде. Это помогает создавать более безопасный и производительный код, который легче понять как во время разработки, так и во время отладки.
int length = name?.Length ?? 0;
Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍13