Performance in .NET Core

Performance in .NET Core

Performance in .NET Core

Всім привіт! Ця стаття є збіркою Best Practices, які я та мої колеги застосовуємо протягом тривалого часу під час роботи на різних проектах.

Інформація про машину, на якій виконувались обчислення:BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100
[Host]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT
Core: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT
[Host]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
Core: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT

Job = Core Runtime = Core

ToList vs ToArray and Cycles


Дану інформацію я планував готувати з виходом .NET Core 3.0, але мене випередили, мені не хочеться красти чужу славу та копіювати чужу інформацію, тому просто вкажу посилання на хорошу статтю, де детально розписано порівняння.

Від себе лише хочу представити вам свої виміри та результати, я додав до них зворотні цикли для любителів “C++ стилю” написання циклів.

код:

public class Bench
    {
        private List<int> _list;
        private int[] _array;

        [Params(100000, 10000000)] public int N;

        [GlobalSetup]
        public void Setup()
        {
            const int MIN = 1;
            const int MAX = 10;
            Random random = new Random();
            _list = Enumerable.Repeat(0, N).Select(i => random.Next(MIN, MAX)).ToList();
            _array = _list.ToArray();
        }

        [Benchmark]
        public int ForList()
        {
            int total = 0;
            for (int i = 0; i < _list.Count; i++)
            {
                total += _list[i];
            }

            return total;
        }
        
        [Benchmark]
        public int ForListFromEnd()
        {
            int total = 0;t
            for (int i = _list.Count-1; i > 0; i--)
            {
                total += _list[i];
            }

            return total;
        }

        [Benchmark]
        public int ForeachList()
        {
            int total = 0;
            foreach (int i in _list)
            {
                total += i;
            }

            return total;
        }

        [Benchmark]
        public int ForeachArray()
        {
            int total = 0;
            foreach (int i in _array)
            {
                total += i;
            }

            return total;
        }

        [Benchmark]
        public int ForArray()
        {
            int total = 0;
            for (int i = 0; i < _array.Length; i++)
            {
                total += _array[i];
            }

            return total;
        }
        
        [Benchmark]
        public int ForArrayFromEnd()
        {
            int total = 0;
            for (int i = _array.Length-1; i > 0; i--)
            {
                total += _array[i];
            }

            return total;
        }
    }

Швидкість роботи в .NET Core 2.2 та 3.0 є майже ідентичними. Ось що мені вдалося отримати в .NET Core 3.0:

Performance in .NET Core

Performance in .NET Core

Ми можемо дійти висновку про те, що циклічна обробка колекції типу Array є швидшою, за рахунок своїх внутрішніх оптимізації та явного виділення розміру колекції. Також варто пам'ятати, що колекція типу List має свої переваги і вам варто використовувати потрібну колекцію в залежності від необхідних обчислень. Навіть якщо ви пишете логіку роботи з циклами не варто забувати, що це звичайний loop і він теж схильний до можливої ​​оптимізації циклів. На habr досить давно вийшла стаття: https://habr.com/ru/post/124910/. Вона все ще актуальна та рекомендується до прочитання.

Кидати

Рік тому я працював у компанії над legacy проектом, у що проект був у рамках нормального обробляти валідацію полів через try-catch-throw конструкцію. Я вже тоді розумів, що це нездорова бізнес-логіка роботи проекту, тому наскільки можна намагався не використовувати таку конструкцію. Але давайте розберемося, чим поганий підхід обробляти помилки такою конструкцією. Я написав невеликий код для того, щоб порівняти два підходи та зняв “бенчі” на кожен варіант.

код:

        public bool ContainsHash()
        {
            bool result = false;
            foreach (var file in _files)
            {
                var extension = Path.GetExtension(file);
                if (_hash.Contains(extension))
                    result = true;
            }

            return result;
        }

        public bool ContainsHashTryCatch()
        {
            bool result = false;
            try
            {
                foreach (var file in _files)
                {
                    var extension = Path.GetExtension(file);
                    if (_hash.Contains(extension))
                        result = true;
                }
                
                if(!result) 
                    throw new Exception("false");
            }
            catch (Exception e)
            {
                result = false;
            }

            return result;
        }

Результати у .NET Core 3.0 та Core 2.2 мають аналогічний результат (.NET Core 3.0):

Performance in .NET Core

Performance in .NET Core

Try catch ускладнює розуміння коду та збільшує час виконання вашої програми. Але якщо вам необхідна дана конструкція, не варто вставляти рядки коду, від яких не очікується обробка помилок - це полегшить розуміння коду. Насправді, навантажує систему не так обробка винятків, скільки викидання самих помилок через конструкцію throw new Exception.

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

Не варто писати конструкцію throw new Exception(), якщо ця ситуація не є винятковою. Обробка та викидання виключення коштує дуже дорого!

ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant

За свій 5-річний досвід роботи на платформі .NET зустрічав чимало проектів, які використовували зіставлення рядків. Також бачив наступну картину: було одне Enterprise рішення з безліччю проектів, кожен із яких виконував порівняння рядків по-різному. Але що варто використати і як це уніфікувати? У книзі CLR via C# Ріхтера я вичитав інформацію про те, що метод ToUpperInvariant() працює швидше за ToLowerInvariant().

Вирізка з книги:

Performance in .NET Core

Звичайно ж, я не повірив і вирішив провести деякі тести тоді ще на .NET Framework і результат мене шокував — понад 15% приросту продуктивності. Далі після приходу на роботу наступного ранку я показав дані виміри своєму начальству і надав їм доступ до вихідців. Після цього 2 з 14 проектів були змінені під нові виміри, а при врахуванні того, що ці два проекти існували щоб обробляти великі excel таблиці, результат був більш ніж значущим для продукту.

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

код:

public const string defaultString =  "VXTDuob5YhummuDq1PPXOHE4PbrRjYfBjcHdFs8UcKSAHOCGievbUItWhU3ovCmRALgdZUG1CB0sQ4iMj8Z1ZfkML2owvfkOKxBCoFUAN4VLd4I8ietmlsS5PtdQEn6zEgy1uCVZXiXuubd0xM5ONVZBqDu6nOVq1GQloEjeRN8jXrj0MVUexB9aIECs7caKGddpuut3";

        [Benchmark]
        public bool ToLower()
        {
            return defaultString.ToLower() == defaultString.ToLower();
        }

        [Benchmark]
        public bool ToLowerInvariant()
        {
            return defaultString.ToLowerInvariant() == defaultString.ToLowerInvariant();
        }

        [Benchmark]
        public bool ToUpper()
        {
            return defaultString.ToUpper() == defaultString.ToUpper();
        }

        [Benchmark]
        public bool ToUpperInvariant()
        {
            return defaultString.ToUpperInvariant() == defaultString.ToUpperInvariant();
        }

Performance in .NET Core

Performance in .NET Core

У .NET Core 3.0 приріст кожного з цих методів ~x2 і балансує реалізації між собою.

Performance in .NET Core

Performance in .NET Core

Tier Compilation

У своїй минулій статті я описав цей функціонал коротко, хотілося б виправити та доповнити свої слова. Багаторівнева компіляція прискорює час запуску вашого рішення, але ви жертвуєте тим, що частини вашого коду компілюватимуться в більш оптимізовану версію в фоні, що може призвести до невеликих витрат. З приходом NET Core 3.0 зменшився час складання проектів з увімкненим tier compilation і пофіксували баги пов'язані з цією технологією. Раніше ця технологія призводила до помилок перших запитів у ASP.NET Core і підвисання при першому складанні в режимі багаторівневої компіляції. На даний момент у .NET Core 3.0 вона включена за замовчуванням, але ви можете відключити її за бажанням. Якщо ви знаходитесь на посаді team-lead, senior, middle або ви керівник відділу, то повинні розуміти, що швидка розробка проекту збільшує цінність команди і дана технологія дозволить вам економити час як розробників, так і сам час роботи проекту.

.NET level up

Підвищуйте версію вашого .NET Framework / .NET Core. Часто кожна нова версія дає додатковий приріст до продуктивності і додає нові фічі.

Але які саме переваги? Давайте розглянемо деякі з них:

  • У .NET Core 3.0 з'явилося R2R образи, які дозволять знизити час запуску .NET Core додатків.
  • З версії 2.2 з'явилася Tier Compilation, завдяки якій програмісти витрачатимуть менше часу на запуск проекту.
  • Підтримка нових стандартів .NET Standard.
  • Підтримує нову версію мови програмування.
  • Оптимізація, з кожною новою версією покращується оптимізація базовими бібліотеками Collection/Struct/Stream/String/Regex та багато чого ще. Якщо ви переходите з .NET Framework на .NET Core, ви отримаєте великий приріст до продуктивності коробки. Для прикладу прикріплюю посилання на частину оптимізацій, які були додані в .NET Core 3.0: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/

Performance in .NET Core

Висновок

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

Посилання на github

Джерело: habr.com

Додати коментар або відгук