Performance in .NET Core

    Performance in .NET Core


    image

    Всем привет! Данная статья является сборником 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++ стиля” написания циклов.

    Code:
    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:





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

    Throw


    Год назад я работал в компании над legacy проектом, в том проекте было в рамках нормального обрабатывать валидацию полей через try-catch-throw конструкцию. Я уже тогда понимал, что это нездоровая бизнес-логика работы проекта, поэтому по возможности старался не использовать такую конструкцию. Но давайте разберёмся, чем же плох подход обрабатывать ошибки такой конструкцией. Я написал небольшой код для того, чтобы сравнить два подхода и снял “бенчи” на каждый вариант.

    Code:
            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):





    Try catch усложняет понимание кода и увеличивает время выполнения вашей программы. Но если вам необходима данная конструкция, не стоит вставлять те строки кода, от которых не ожидается обработка ошибок — это облегчит понимание кода. На самом деле, нагружает систему не столько обработка исключений, сколько выкидывание самих ошибок через конструкцию throw new Exception.

    Выкидывание исключений работает медленнее какого-нибудь класса, который соберёт ошибку в нужном формате. Если вы обрабатываете форму или какие-нибудь данные и явно знаете какая ошибка должна быть, так почему бы не обработать их?

    Не стоит писать конструкцию throw new Exception() если эта ситуация не является исключительной. Обработка и выкидывание исключения стоит очень дорого!!!

    ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant


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

    Вырезка из книги:



    Конечно же я не поверил и решил провести некоторые тесты тогда ещё на .NET Framework и результат меня шокировал — более 15% прироста производительности. Далее по приходу на работу следующим утром я показал данные замеры своему начальству и предоставил им доступ к исходникам. После этого 2 из 14 проектов были изменены под новые замеры, а при учёте того что эти два проекта существовали чтобы обрабатывать огромные excel таблицы, результат был более чем значимым для продукта.

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

    Code:
    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();
            }
    






    В .NET Core 3.0 прирост для каждого из этих методов ~x2 и балансирует реализации между собой.





    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/



    Заключение


    При написание кода стоит уделять внимание разным аспектам вашего проекта и использовать функции вашего языка программирования и платформы для достижения наилучшего результата. Буду рад если вы поделелитесь своими знаниями связанными с оптимизацией в .NET.

    Ссылка на github
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      +3

      Дико смотрятся сравнение строк через ToLowerInvariant в статье про перформанс. Надеюсь автор знает что это далеко не самое оптимальное решение и просто забыл это упомянуть.

        0
        Я могу ошибаться, но, вроде, StringComparer, который предоставляет сравнение без учета регистра тоже за кулисами делает приведение к одному регистру.
          +4
          Вот откуда берутся эти «догадки», весь .NET открыт для изучения:

          TL;DN
          Нет, всё упирается в нативный код платформы


          String.Equals
          public bool Equals(string value, StringComparison comparisonType)
          {
          	if (comparisonType < StringComparison.CurrentCulture || comparisonType > StringComparison.OrdinalIgnoreCase)
          	{
          		throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
          	}
          	if ((object)this == value)
          	{
          		return true;
          	}
          	if (value != null)
          	{
          		switch (comparisonType)
          		{
          		case StringComparison.CurrentCulture:
          			return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, CompareOptions.None) == 0;
          		case StringComparison.CurrentCultureIgnoreCase:
          			return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, CompareOptions.IgnoreCase) == 0;
          		case StringComparison.InvariantCulture:
          			return CultureInfo.InvariantCulture.CompareInfo.Compare(this, value, CompareOptions.None) == 0;
          		case StringComparison.InvariantCultureIgnoreCase:
          			return CultureInfo.InvariantCulture.CompareInfo.Compare(this, value, CompareOptions.IgnoreCase) == 0;
          		case StringComparison.Ordinal:
          			if (Length != value.Length)
          			{
          				return false;
          			}
          			return EqualsHelper(this, value);
          		case StringComparison.OrdinalIgnoreCase:
          			if (Length != value.Length)
          			{
          				return false;
          			}
          			if (IsAscii() && value.IsAscii())
          			{
          				return EqualsIgnoreCaseAsciiHelper(this, value);
          			}
          			return TextInfo.CompareOrdinalIgnoreCase(this, value) == 0;
          		default:
          			throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
          		}
          	}
          	return false;
          }


          для ordinal ASCII строк:
          private unsafe static bool EqualsIgnoreCaseAsciiHelper(string strA, string strB)
          {
          	int num = strA.Length;
          	fixed (char* ptr = &strA.m_firstChar)
          	{
          		fixed (char* ptr3 = &strB.m_firstChar)
          		{
          			char* ptr2 = ptr;
          			char* ptr4 = ptr3;
          			while (true)
          			{
          				if (num == 0)
          				{
          					return true;
          				}
          				int num2 = *ptr2;
          				int num3 = *ptr4;
          				if (num2 != num3 && ((num2 | 0x20) != (num3 | 0x20) || (uint)((num2 | 0x20) - 97) > 25u))
          				{
          					break;
          				}
          				ptr2++;
          				ptr4++;
          				num--;
          			}
          			return false;
          		}
          	}
          }
          


          Для остальных строк:

          INT32 QCALLTYPE COMNlsInfo::InternalCompareStringOrdinalIgnoreCase(
              LPCWSTR string1, INT32 index1,
              LPCWSTR string2, INT32 index2,
              INT32 length1,
              INT32 length2)
          {
              CONTRACTL
              {
                  QCALL_CHECK;
                  PRECONDITION(CheckPointer(string1));
                  PRECONDITION(CheckPointer(string2));
              } CONTRACTL_END;
          
              INT32 result = 0;
          
              BEGIN_QCALL;
              //
              //  Get the arguments.
              //  We assume the caller checked them before calling us
              //
          
              // We don't allow the -1 that native code allows
              _ASSERT(length1 >= 0);
              _ASSERT(length2 >= 0);
          
              // Do the comparison
          #ifndef FEATURE_CORECLR
              AppDomain* curDomain = GetAppDomain();
              
              if (curDomain->m_pCustomSortLibrary != NULL) {
                  result = (curDomain->m_pCustomSortLibrary->pCompareStringOrdinal)(string1 + index1, length1, string2 + index2, length2, TRUE);
              } 
              else 
          #endif
              {
                  result = NewApis::CompareStringOrdinal(string1 + index1, length1, string2 + index2, length2, TRUE);
              }
          
              // The native call shouldn't fail
              _ASSERT(result != 0);
              if (result == 0)
              {
                  // return value of 0 indicates failure and error value is supposed to be set.
                  // shouldn't ever really happen
                  _ASSERTE(!"catastrophic failure calling NewApis::CompareStringOrdinal!  This is usually due to bad arguments.");
              }
          
              // Adjust the result to the expected -1, 0, 1 result
              result -= 2;
          
              END_QCALL;
          
              return result;
          }
          
          
          


          который вызывает WinAPI CompareStringOrdinal

          Для Invariant culture строк:

          INT32 QCALLTYPE COMNlsInfo::InternalCompareString(
              INT_PTR handle,
              INT_PTR handleOrigin,
              LPCWSTR localeName,
              LPCWSTR string1, INT32 offset1, INT32 length1,
              LPCWSTR string2, INT32 offset2, INT32 length2,
              INT32 flags)
          {
              CONTRACTL
              {
                  QCALL_CHECK;
                  PRECONDITION(CheckPointer(string1));
                  PRECONDITION(CheckPointer(string2));
                  PRECONDITION(CheckPointer(localeName));
              } CONTRACTL_END;
          
              INT32 result = 1;
              BEGIN_QCALL;
          
              handle = EnsureValidSortHandle(handle, handleOrigin, localeName);
          
          #ifndef FEATURE_CORECLR
              AppDomain* curDomain = GetAppDomain();
          
              if(!(curDomain->m_bUseOsSorting))
              {
                  result = SortVersioning::SortDllCompareString((SortVersioning::PSORTHANDLE) handle, flags, &string1[offset1], length1, &string2[offset2], length2, NULL, 0);
              }
              else if (curDomain->m_pCustomSortLibrary != NULL) {
                  result = (curDomain->m_pCustomSortLibrary->pCompareStringEx)(handle != NULL ? NULL : localeName, flags, &string1[offset1], length1, &string2[offset2], length2, NULL, NULL, (LPARAM) handle);
              } 
              else 
          #endif
              {
                  result = NewApis::CompareStringEx(handle != NULL ? NULL : localeName, flags, &string1[offset1], length1, &string2[offset2], length2,NULL,NULL, (LPARAM) handle);
              }
          
              switch (result)
              {
                  case CSTR_LESS_THAN:
                      result = -1;
                      break;
          
                  case CSTR_EQUAL:
                      result = 0;
                      break;
          
                  case CSTR_GREATER_THAN:
                      result = 1;
                      break;
          
                  case 0:
                  default:
                      _ASSERTE(!"catastrophic failure calling NewApis::CompareStringEx!  This could be a CultureInfo, RegionInfo, or Calendar bug (bad localeName string) or maybe a GCHole.");
                      break;
              }
          
              END_QCALL;
              return result;
          }


          Который заканчивается в WinAPI CompareStringEx

          0
          А каким будет самое оптимальное решение?
            +4
            В кейсе автора
            string.Equals(xxx, StringComparison.InvariantCultureIgnoreCase)

            Но автор не указал, что хотел именно культурно-инвариантное сравнение (а только то, что хотел более быстрый вариант),
            тогда StringComparison.OrdinalIgnoreCase подойдет лучше.

            В остальных кейсах надо смотреть. Но для сравнения строк, ToUpper/ToLower всегда будут неправильным решением.
          0
          Можно было б еще Sum из LINQ добавить в тест массивов и списков.
            0

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


            И еще, чем автору не угодил стандартный String.Equals, что понадобились такие извращения с приведением к верхнему регистру с аллокацией новых объектов?

              +1
              Не очень понятен данный абзац:
              Если вы находитесь на должности team-lead, senior, middle или вы руководитель отдела то, должны понимать что быстрая разработка проекта увеличивает ценность команды и данная технология позволит вам экономить время как разработчиков, так и само время работки проекта.

              Как это связано со скоростью разработки? Или это описка и имеется ввиду сборка проекта?
              Да, и оно все же называется Tiered compilation

              С версии 2.2 появилась Tier Compilation, благодаря которой программисты будут тратить меньше времени на запуск проекта.

              TC доступен и в 2.1, но его нужно включать. В 2.2 включен TC по умолчанию.
                +1
                при учёте того что эти два проекта существовали чтобы обрабатывать огромные excel таблицы, результат был более чем значимым для продукта.

                Насколько значимым? Чтобы такая мелочь заметно повлияла на реальную жизнь нужен какой-то весьма специфический продукт.
                  0

                  В условиях NDA соглашения я не могу рассказать, что именно разрабатывал, но этот проект используется в огромной бюджетной системе одной страны где выгрузка в Excel и сбор многих данных (приблизительно от 10 до 1000 тысяч), которые нужно привести к общему виду и сравнить по определенным условиям. Т.к. сравнение было обычным делом в каждом из отчётов, прирост был очень значим.

                    0
                    «в огромной бюджетной системе одной страны где выгрузка в Excel и сбор многих данных»

                    Кривая хрень, короче. Hadoop, Spark, не, не слышали ёксЕль!

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое