Ось кілька поширених помилок, які можуть призвести до поганої роботи вашої програми. Але який сенс знати помилки, якщо ми не знаємо, як їх виправити або уникнути? Ось чому я надаю вам кілька порад прямо тут.

Постійний доступ до одних і тих самих даних

У програмах на C# досить часто доводиться робити багаторазові запити до бази даних не тільки для отримання оперативної інформації, необхідної для завершення варіанту використання, але й для отримання інформації, яка рідко змінюється. Ми розглянемо останній випадок

Уявіть, що у вас є таблиця, в якій зберігаються облікові дані для автентифікації у сторонньому сервісі. Зазвичай, перш ніж користуватися сервісом, потрібно пройти автентифікацію. У вашій програмі, ймовірно, є процедура для отримання цих облікових даних з таблиці у вашій базі даних. Це може не здатися проблемою, якщо процедура виконується нечасто або лише кілька разів на день. Однак, у більш широкому випадку використання, коли вам потрібно використовувати сторонній сервіс частіше, це може стати проблемою продуктивності вашого додатку з часом. Враховуючи, що облікові дані - це інформація, яка не оновлюється часто, ви можете використовувати кеш у пам'яті програми.

У останніх версіях .NET це можна досить просто і швидко реалізувати. Розглянемо наступний сценарій.

public async Task<List<AccountSenders>> GetAccountSenders()
{
  return await _context.AccountSenders.ToListAsync();
}

У вищенаведеному коді ми отримуємо список облікових даних з таблиці нашої бази даних. Сам по собі цей метод, здається, не створює проблем з продуктивністю. Однак, якщо врахувати, що ми використовуємо наш сторонній сервіс багато разів у нашому додатку, використання цього методу збільшується, що призводить до непотрібних запитів до бази даних. Це відбувається тому, що облікові дані не змінюються або не будуть змінюватися так часто, як ми автентифікуємось. Розглянемо наступний підхід з використанням кешу в пам'яті, що надається .NET.

public async Task<List<AccountSenders>?> GetAccountSenders(IMemoryCache _cache)
{
    string cacheKey = "AccountSenders";

    if (!_cache.TryGetValue(cacheKey, out List<AccountSenders>? accountSenders))
    {
        accountSenders = await _context.AccountSenders.ToListAsync();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(5));

         _cache.Set(cacheKey, accountSenders, cacheEntryOptions);
    }
    return accountSenders;
}

Ви, напевно, думаєте, що більша кількість коду не обов'язково означає кращу продуктивність, але давайте зазирнемо за лаштунки…

Ось де в гру вступає кешування. Це спосіб зберігання даних у пам'яті, щоб вам не доводилося отримувати їх щоразу. І хоча спочатку його налаштування може здатися дещо складнішим, повірте, воно того варте.

Повертаючись до попереднього коду, перед запитом до бази даних ми перевіряємо, чи ця інформація вже зберігається в кеші. При цьому метод також враховує час закінчення терміну дії кешу, що є дуже важливою властивістю. Продовжуючи виконувати процедуру, ми помічаємо, що якщо інформація не знайдена в кеші за вказаним ім'ям, то ми можемо перейти до запиту до бази даних. Після цього ми можемо зберегти цю інформацію в кеші, щоб наступного разу повторити перевірку і отримати інформацію з кешу, а не з бази даних.

Також зверніть увагу, що при налаштуванні кешу ми встановлюємо час закінчення терміну дії, який має вирішальне значення, оскільки кеш повинен мати його. Після закінчення цього часу ми можемо знову зробити запит до бази даних, оскільки, зрештою, ми завжди хочемо мати актуальну інформацію.

Отже, ви зрозуміли, що це простий і зрозумілий спосіб, який може значно допомогти вам у виконанні часто виконуваних запитів. Крім того, це практичний спосіб почати реалізацію кешування у ваших .NET додатках.

Поганий екземпляр HttpClient - тихий ворог продуктивності

Розглядаючи рішення для зв'язку із зовнішнім сервісом через протокол HTTP, ми майже напевно дійдемо висновку, що для цього потрібно використовувати HttpClient. Однак, що криється за створенням простого екземпляру HttpClient, який може стати тихим ворогом продуктивності нашого додатку? Ну, майже все! Кожен раз, коли створюється екземпляр HttpClient, відкриваються нові TCP-з'єднання, які споживають ресурси і потенційно створюють вузькі місця, особливо при високих навантаженнях.

public async Task<string> GetAsync(string url)
{
  using (var client = new HttpClient())
  {
    var response = await client.GetAsync(url);
    return await response.Content.ReadAsStringAsync();
  }
}

У вищенаведеному коді, здається, немає жодної проблеми, це простий екземпляр HttpClient, який робить запит. Хоча використання цього методу здається простим, його неправильна інстанціалізація або неадекватне використання може стати першопричиною безлічі проблем з продуктивністю у вашому додатку.

Використання HttpClientFactory для створення єдиного спільного екземпляра HttpClient є рекомендованою практикою.

public async Task<string> GetAsync(string url)
{
  using (var client = _httpClientFactory.CreateClient())
  {
    var response = await client.GetAsync(url);
    return await response.Content.ReadAsStringAsync();
  }
}

За допомогою HttpClientFactory ви можете створювати HTTP-клієнтів, належним чином налаштованих і керованих самим фреймворком. Ці клієнти, створені за допомогою HttpClientFactory, зберігаються у пулі, тому нові клієнти не створюються щоразу, коли вони потрібні. Крім того, коли клієнт більше не використовується, він повертається до пулу і очищається для повторного використання.

Підсумовуючи, неправильне створення екземплярів HttpClient або використання його без належної конфігурації може негативно вплинути на продуктивність вашого додатку. Використання HttpClientFactory і правильна конфігурація можуть значно підвищити швидкість і ефективність ваших HTTP-запитів.

Конкатенація рядків

Конкатенація рядків у .NET може вплинути на продуктивність порівняно з використанням StringBuilder через спосіб обробки рядків у пам'яті

При конкатенації рядків за допомогою оператора + або String.Concat() методу, .NET створює новий об'єкт рядка кожного разу, коли відбувається операція конкатенації. Це означає, що виділення пам'яті відбувається часто, особливо в сценаріях, де виконується кілька конкатенацій в циклах або при великих маніпуляціях з рядками. Кожен новий рядковий об'єкт вимагає виділення та звільнення пам'яті, що може призвести до фрагментації пам'яті та збільшення накладних витрат на збирання сміття

З іншого боку, StringBuilder надає ефективніший спосіб маніпулювання рядками, особливо при об'єднанні декількох рядків або виконанні повторюваних маніпуляцій з рядками. StringBuilder використовує буфер змінного розміру для зберігання рядкових даних, що мінімізує виділення пам'яті та зменшує накладні витрати, пов'язані зі створенням нових рядкових об'єктів.

Наводимо приклад, який ілюструє цю різницю:

// Using string concatenation
string result = "";
for (int i = 0; i < 10000; i++) {
    result += i.ToString();  // Each concatenation creates a new string object
}

// Using StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.Append(i.ToString());  // StringBuilder appends data to its internal buffer
}
string result = sb.ToString();  // Convert StringBuilder to string when needed

У вищенаведеному прикладі багаторазове використання конкатенації рядків (+=) всередині циклу створює новий об'єкт рядка для кожної операції конкатенації, що призводить до низької продуктивності та збільшення використання пам'яті. Натомість використання методу StringBuilder's Append() ефективно додає дані у внутрішній буфер, уникаючи зайвого виділення пам'яті та підвищуючи продуктивність.

Підсумовуючи, можна сказати, що хоча конкатенація рядків може здатися зручною, особливо для невеликих операцій, для оптимізації використання пам'яті та продуктивності додатків у .NET важливо використовувати StringBuilder для більших маніпуляцій з рядками або сценаріїв, де продуктивність має вирішальне значення

Винятки та NULL

Виникнення занадто великої кількості винятків і неправильне використання повернення null може вплинути на продуктивність .NET-додатка через накладні витрати, пов'язані з обробкою винятків і перевіркою на нуль.

Часте генерування винятків, особливо у критичних до продуктивності ділянках коду, може призвести до значного зниження продуктивності. Обробка винятків передбачає захоплення трасування стеку, розмотування стеку викликів та інші операції, які споживають цикли процесора та пам'ять

Приклад:

try {
    // Code that might throw exceptions
} catch (Exception ex) {
    // Exception handling logic
}

У наведеному вище прикладі, якщо код у блоці try часто генерує виключення, накладні витрати на обробку цих винятків можуть стати помітними, що вплине на загальну продуктивність програми.

Неправильне використання повернення нуля

Повернення значення null замість належної обробки нульових значень може призвести до винятків за нульовим посиланням (NRE), коли доступ до повернутого значення здійснюється без належних перевірок на нуль. Це може призвести до помилок і неочікуваної поведінки у програмі, а також вимагає додаткових перевірок на нуль у коді, що може вплинути на продуктивність.

Приклад:

public string GetCustomerName(int customerId) {
    // Some logic to fetch customer name
    if (customerExists) {
        return customerName;
    } else {
        return null; // Incorrect usage if caller does not handle null properly
    }
}

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

Щоб пом'якшити ці проблеми з продуктивністю:

Виконайте такі дії

  • Використовуйте винятки
  • Використовуйте винятки для виняткових умов, а не для звичайного потоку керування.
  • Мінімізуйте використання винятків у критичних до продуктивності ділянках коду.
  • Обробляйте очікувані умови помилок, використовуючи коди повернення або інші механізми замість винятків.
  • Переконайтеся, що методи, які повертають null, чітко документують можливість повернення нульових значень, і викликачі обробляють їх належним чином.
    • Дотримуючись цих рекомендацій, ви зможете підвищити продуктивність і надійність ваших .NET-додатків.

    Висновок

    При розробці додатків .NET часто не звертають уваги на питання продуктивності, вважаючи, що незначні недоліки не матимуть суттєвого впливу на функціональність програми. Однак така самовпевненість може призвести до поступового накопичення неоптимальних практик, які в сукупності з часом погіршують продуктивність програми. Хоча спочатку може здатися неважливим визначати пріоритети інших аспектів розробки, нехтування міркуваннями продуктивності може призвести до серйозних наслідків, коли додаток масштабується або стикається зі збільшенням обсягу використання.

    Не дивлячись на привабливість доцільності в розробці, важливо визнати, що кожен випадок поганої практики, що призводить до зниження продуктивності, сприяє загальному погіршенню продуктивності програми. Чи то неефективні запити до бази даних, чи то надмірне генерування винятків, чи то неправильні маніпуляції з рядками - ці, здавалося б, незначні прогалини в оптимізації можуть призвести до значного зниження продуктивності. Для розробників вкрай важливо розвивати культуру свідомості продуктивності, де міркування щодо оптимізації інтегровані в кожну стадію життєвого циклу розробки.

    Зрештою, довгостроковий успіх і життєздатність .NET-додатків залежить від колективної старанності розробників, які з самого початку визначають пріоритетність міркувань продуктивності. Заохочуючи проактивний підхід до оптимізації продуктивності та дотримуючись найкращих практик, розробники можуть зменшити ризик створення додатків, що знижують продуктивність. Завдяки узгодженим зусиллям, спрямованим на усунення вузьких місць у роботі та дотримання стандартів оптимізації, розробники можуть гарантувати, що їхні .NET-додатки забезпечують оптимальну продуктивність, масштабованість і зручність роботи користувачів в умовах мінливих вимог і шаблонів використання.

    Щасливого кодування!