Сегодня мы рассмотрим, как небольшие оптимизации в нужных местах приложения могут значительно улучшить его производительность.
Убрали создание лишнего итератора в одном месте, избавились от упаковки в другом и в итоге получаем несоизмеримый с правками результат. Через всю статью будет проходить одна простая, хотя и не новая мысль, которую я прошу вас запомнить.
Преждевременная оптимизация — это зло.
Позволь мне объяснить.
Бывает, что оптимизация и читабельность идут немного в разные стороны.
Код может работать лучше, но его труднее читать и поддерживать.
И наоборот — код легко читается и модифицируется, но есть некоторые проблемы с производительностью.
Поэтому важно понимать, чем вы готовы пожертвовать в том или ином случае.
Вы можете прочитать статью, броситься редактировать кодовую базу проекта и.
не получить никакого улучшения производительности.
Но ваш код станет более сложным.
Именно поэтому (и, впрочем, это всегда лучше) следует подходить к делу с холодной головой.
Замечательно, если вы знаете узкие места приложения, где вам может помочь оптимизация.
Если вы еще не знаете таких мест, вам на помощь придут разного рода профайлеры.
Они могут предоставить много информации о вашем приложении.
В частности, опишите его поведение во времени: экземпляры какого типа создаются чаще всего, сколько времени приложение тратит на сборку мусора, сколько времени занимает выполнение того или иного фрагмента кода и т. д. Здесь я хочу похвалить два инструмента JetBrains: точкатраце И точкаМемори .
Работать с ними удобно и, как правило, интуитивно понятно, информации много, она отлично визуализируется.
JetBrains, вы потрясающие! Но вернемся ближе к нашим оптимизациям.
На протяжении статьи мы разберем несколько случаев, с которыми мы столкнулись и которые показались мне наиболее интересными.
Каждое из описанных изменений дало положительный результат, так как они были внесены в отмеченные профилировщиками узкие места приложения.
К сожалению, результатов изменения производительности от каждой конкретной правки у меня нет, но общий результат оптимизации будет приведен в конце статьи.
Примечание .
Эта статья о работе с .
NET Framework. Как показывает практика (см.
пример с Enum.GetHashCode ), в некоторых случаях работа одного и того же куска кода на C# под .
NET Core/.
NET может быть более оптимальной, чем под .
NET Framework.
Что именно мы оптимизируем?
Советы, описанные в статье, актуальны для любого .NET-приложения.
Конечно, как я писал выше, правки кода будут особенно полезны, когда они производятся в узких местах приложения.
Сразу хочу отметить, что никаких абстрактных теоретических дискуссий мы вести не будем.
В этом ключе советы в духе «изменить код, чтобы сохранить создание одного итератора» выглядели бы максимально странно.
Все проблемы, о которых мы сегодня поговорим, были выявлены по результатам профилирования.
статический анализатор PVS-Studio для С#.
Основная цель профилирования заключалась в сокращении времени анализа.
После начала работы быстро выяснилось, что у анализатора большие проблемы со сборкой мусора: это занимало значительное количество времени.
На самом деле мы знали это и раньше, просто в очередной раз убедились.
Кстати, ранее был сделан ряд оптимизаций анализатора, о которых есть отдельная статья .
Однако проблема по-прежнему оставалась актуальной.
Посмотрите на скриншот ниже ( полноразмерное изображение здесь ).
Это результат профилирования C#-анализатора PVS-Studio, полученный в ходе анализа одного из проектов.
8 полос — 8 потоков, которые использовал анализатор.
Как видите, в каждом потоке значительную часть времени занимала работа сборщика мусора.
Отвергнув совет переписать всё на C, мы стали более детально изучать информацию по результатам профилирования и конкретно исключать создание ненужных/временных объектов.
К нашей радости, такой подход сразу начал приносить свои плоды.
Собственно, эта тема будет сегодня в центре внимания.
Какие успехи были достигнуты? Об этом я вам обязательно расскажу, но от интриги воздержусь до конца статьи.
Вызов методов с параметром params
Методы, сигнатура которых объявляет параметры -параметр, в качестве соответствующего аргумента может быть:- не принимайте ценности;
- принять одно или несколько значений.
Давайте посмотрим на его представление в IL-коде:static void ParamsMethodExample(params String[] stringValue)
.
method private hidebysig static void
ParamsMethodExample(string[] stringValue) cil managed
{
.
param [1]
.
custom instance void
[mscorlib]System.ParamArrayAttribute::.
ctor() = ( 01 00 00 00 )
.
}
Как мы видим, это простой метод с одним параметром, отмеченным атрибутом System.ParamArrayAttribute .
Тип параметра — указанный нами строковый массив.
Интересен тот факт .
Вы не можете использовать этот атрибут напрямую — компилятор выдаст ошибку CS0674 и заставит вас использовать ключевое слово параметры .
Из приведенного выше IL-кода следует очень простой вывод — каждый раз, когда нам нужно вызвать этот метод, вызывающему коду сначала нужно будет создать массив.
Почти.
Чтобы лучше понять, что происходит, когда этот метод вызывается с разными аргументами, давайте рассмотрим конкретные примеры.
Первый вызов без аргументов.
ParamsMethodExample()
ИЛ-код: call !!0[] [mscorlib]System.Array::Empty<string>()
call void Optimizations.Program::ParamsMethodExample(string[])
Как мы помним, метод ожидает на входе массив, поэтому его нужно откуда-то взять.
В этом случае в качестве аргумента используется результат вызова статического метода.
System.Array.Empty .
Применение Массив.
Пустой позволяет сэкономить на создании пустых коллекций и как следствие снижает нагрузку на GC. А теперь ложка дегтя.
Более старые версии компилятора могут генерировать другой IL-код. Например, вот так: ldc.i4.0
newarr [mscorlib]System.String
call void Optimizations.Program::ParamsMethodExample(string[])
В этом случае для каждого вызова метода, когда для параметры -в параметре отсутствует соответствующий аргумент, будет создан новый пустой массив.
Время выполнения задачи самопроверки.
Отличаются ли следующие два вызова, и если да, то как? ParamsMethodExample(null);
ParamsMethodExample(String.Empty);
Если вы уже дали себе ответ на этот вопрос, давайте разберемся.
Начнем с вызова, когда аргумент явный нулевой : ParamsMethodExample(null);
ИЛ-код: ldnull
call void Optimizations.Program::ParamsMethodExample(string[])
В этом случае создания массива не происходит. Значение просто передается в качестве аргумента нулевой .
Теперь рассмотрим случай, когда в метод передается ненулевое значение: ParamsMethodExample(String.Empty);
ИЛ-код: ldc.i4.1
newarr [mscorlib]System.String
dup
ldc.i4.0
ldsfld string [mscorlib]System.String::Empty
stelem.ref
call void Optimizations.Program::ParamsMethodExample(string[])
Как видите, кода здесь уже больше, чем в предыдущих примерах.
Перед вызовом метода создается массив.
Этот массив будет содержать все аргументы метода, соответствующие параметры -параметр.
В этом случае в массив будет записано одно значение — пустая строка.
Обратите внимание: если имеется несколько аргументов, также будет создан массив, даже если аргументы являются явными значениями.
нулевой .
Таким образом, вызовы методов с параметры -параметры могут стоить вам немалых денег из-за неявного создания массивов.
В некоторых случаях, например, когда соответствующий аргумент отсутствует и используется современный компилятор, вызов можно оптимизировать.
Но вообще стоит помнить о временных объектах.
Профилируя анализатор, я нашел несколько мест, где профайлер обнаружил создание большого количества массивов, которые быстро попадали под сборку мусора.
Соответствующие методы содержали примерно следующий код: bool isLoop = node.IsKindEqual(SyntaxKind.ForStatement,
SyntaxKind.ForEachStatement,
SyntaxKind.DoStatement,
SyntaxKind.WhileStatement);
Сам метод IsKindEqual выглядело так: public static bool IsKindEqual(this SyntaxNode node, params SyntaxKind[] kinds)
{
return kinds.Any(kind => node.IsKind(kind));
}
Из теории, которую мы рассмотрели выше, следует, что для вызова метода нам необходимо создать массив.
Сам массив используется только для однократного обхода его, после чего он больше не нужен.
Можно ли здесь избавиться от создания ненужных массивов? Эбуквально: bool isLoop = node.IsKind(SyntaxKind.ForStatement)
|| node.IsKind(SyntaxKind.ForEachStatement)
|| node.IsKind(SyntaxKind.DoStatement)
|| node.IsKind(SyntaxKind.WhileStatement);
Эта простая правка помогла уменьшить количество временно создаваемых массивов и, как следствие, снизить нагрузку на GC. Примечание .
Библиотеки .
NET иногда используют хитрый трюк.
Для некоторых методов с параметры -параметры также имеют перегрузки, которые вместо параметры -параметры принимают 1, 2, 3 параметра соответствующего типа.
Это позволяет избежать накладных расходов на создание временных массивов на стороне вызывающей стороны.
Перечисляемый.
Любой Неоднократно в результатах профилирования мелькал вызов метода Любой .
Что случилось с ним? Разберемся на основе реального кода — уже упомянутый метод IsKindEqual .
Раньше мы больше внимания уделяли параметры -параметр.
Теперь более подробно рассмотрим код метода изнутри.
public static bool IsKindEqual(this SyntaxNode node, params SyntaxKind[] kinds)
{
return kinds.Any(kind => node.IsKind(kind));
}
Чтобы понять, в чем проблема Любой , нужно заглянуть «под капот» метода.
Возьмем исходный код из нашего любимого referencesource.microsoft.com .
public static bool Any<TSource>(this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
if (source == null)
throw Error.ArgumentNull("source");
if (predicate == null)
throw Error.ArgumentNull("predicate");
foreach (TSource element in source)
{
if (predicate(element))
return true;
}
return false;
}
Исходная коллекция просто перебирается в цикле.
для каждого.
И если хотя бы для одного элемента вызов предикат возвращаемое значение истинный , то результатом метода будет истинный , в противном случае - ЛОЖЬ .
Основная проблема здесь в том, что любая входная коллекция интерпретируется именно так: IEnumerable , и нет оптимизации для конкретных типов коллекций.
Напомню, что в рассматриваемом случае мы работаем с массивом.
Эксперты уже догадались, что основная проблема с Любой — создание дополнительного итератора для обхода коллекции.
Если вы не совсем понимаете, в чем проблема, не волнуйтесь, сейчас разберемся.
Отрежем ненужные фрагменты метода Любой и упростим его, оставим нужный нам основной код: цикл для каждого и объявление коллекции, с которой работает цикл.
В результате рассмотрим следующий код: static void ForeachTest(IEnumerable<String> collection)
{
foreach (var item in collection)
Console.WriteLine(item);
}
Соответствующий IL-код: .
method private hidebysig static void ForeachTest( class [mscorlib]System.Collections.Generic.IEnumerable`1<string> collection) cil managed { .
maxstack 1 .
locals init ( [0] class [mscorlib]System.Collections.Generic.IEnumerator`1<string> V_0) IL_0000: ldarg.0 IL_0001: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<string>::GetEnumerator() IL_0006: stloc.0 .
try
{
IL_0007: br.s IL_0014
IL_0009: ldloc.0
IL_000a: callvirt instance !0 class
[mscorlib]System.Collections.Generic.IEnumerator`1<string>::get_Current()
IL_000f: call void [mscorlib]System.Console::WriteLine(string)
IL_0014: ldloc.0
IL_0015: callvirt instance bool
[mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001a: brtrue.s IL_0009
IL_001c: leave.s IL_0028
}
finally
{
IL_001e: ldloc.0
IL_001f: brfalse.s IL_0027
IL_0021: ldloc.0
IL_0022: callvirt instance void
[mscorlib]System.IDisposable::Dispose()
IL_0027: endfinally
}
IL_0028: ret
}
Как видите, здесь много чего происходит. Поскольку компилятор ничего не знает о том, каким фактическим типом представлена коллекция, он генерирует общий код для прохождения коллекции через итератор.
Получение итератора происходит через вызов метода GetEnumerator (метка IL_0001).
Получение итератора для массива (наш особый случай) путем вызова метода GetEnumerator приводит к созданию объекта в куче, и все дальнейшее взаимодействие с коллекцией строится на использовании этого объекта.
Примечание .
Компилятор может применять специальные оптимизации при получении итератора для пустого массива.
В этом случае вызов GetEnumerator не приведет к созданию нового объекта.
Но, возможно, когда-нибудь я напишу об этом отдельную заметку.
В общем случае рассчитывать на такую оптимизацию явно не стоит. Теперь немного изменим код, чтобы компилятор точно знал, что мы работаем с массивом.
Код C# будет выглядеть так: static void ForeachTest(String[] collection)
{
foreach (var item in collection)
Console.WriteLine(item);
}
Соответствующий код IL: .
method private hidebysig static void ForeachTest(string[] collection) cil managed { // Code size 25 (0x19) .
maxstack 2 .
locals init ([0] string[] V_0,
[1] int32 V_1)
IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_0012
IL_0006: ldloc.0
IL_0007: ldloc.1
IL_0008: ldelem.ref
IL_0009: call void [mscorlib]System.Console::WriteLine(string)
IL_000e: ldloc.1
IL_000f: ldc.i4.1
IL_0010: add
IL_0011: stloc.1
IL_0012: ldloc.1
IL_0013: ldloc.0
IL_0014: ldlen
IL_0015: conv.i4
IL_0016: blt.s IL_0006
IL_0018: ret
}
В этом случае, когда компилятор точно знает, с какой коллекцией мы работаем, он генерирует гораздо более простой код. В частности, пропала всякая работа с итератором — даже объект не создается (снижаем нагрузку на GC).
Кстати, вот вам вопрос для самопроверки.
Если мы реконструируем код C# из этого IL-кода, какую языковую конструкцию мы получим в итоге? Как видите, этот код сильно отличается от того, что было сгенерировано для цикла.
для каждого ранее.
Внимание, ответьте.
Для представленного ниже метода на C# будет сгенерирован тот же IL-код, что и тот, который мы рассмотрели выше (за исключением имен): static void ForeachTest2(String[] collection)
{
String[] localArr;
int i;
localArr = collection;
for (i = 0; i < localArr.Length; ++i)
Console.WriteLine(localArr[i]);
}
Таким образом, когда компилятор знает, что мы работаем с массивом, он генерирует более оптимальный код, расширяя цикл для каждого Как для .
К сожалению, при использовании Любой в нашем случае мы теряем такую оптимизацию, а также создаем дополнительный объект-итератор для обхода последовательности.
Лямбда-выражения
Лямбды — очень удобная штука, значительно облегчающая жизнь.Конечно, пока не придет кто-то, кто решит запихнуть лямбду внутрь лямбды, внутрь лямбды.
Те, кто любит этим заниматься, одумаются, серьезно.
В общем, использование лямбда-выражений значительно упрощает жизнь.
Но не забывайте, что под капотом лямбды разрастаются на целые классы.
Как следствие, экземпляры этих же классов все равно необходимо создавать.
Давайте еще раз посмотрим на метод IsKindEqual .
public static bool IsKindEqual(this SyntaxNode node, params SyntaxKind[] kinds)
{
return kinds.Any(kind => node.IsKind(kind));
}
Теперь давайте посмотрим на соответствующий IL-код: .
method public hidebysig static bool IsKindEqual( class [Microsoft.CodeAnalysis]Microsoft.CodeAnalysis.SyntaxNode node, valuetype [Microsoft.CodeAnalysis.CSharp]Microsoft.CodeAnalysis.CSharp.SyntaxKind[] kinds) cil managed { .
custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute:: .
ctor() = ( 01 00 00 00 ) .
param [2] .
custom instance void [mscorlib]System.ParamArrayAttribute:: .
ctor() = ( 01 00 00 00 ) // Code size 32 (0x20) .
maxstack 3 .
locals init (class OptimizationsAnalyzer.SyntaxNodeUtils/'<>c__DisplayClass0_0' V_0) IL_0000: newobj instance void OptimizationsAnalyzer.SyntaxNodeUtils/'<>c__DisplayClass0_0'::.
ctor() IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: ldarg.0 IL_0008: stfld class [Microsoft.CodeAnalysis]Microsoft.CodeAnalysis.SyntaxNode OptimizationsAnalyzer.SyntaxNodeUtils/'<>c__DisplayClass0_0'::node IL_000d: ldarg.1 IL_000e: ldloc.0 IL_000f: ldftn instance bool OptimizationsAnalyzer.SyntaxNodeUtils/'<>c__DisplayClass0_0' ::'<IsKindEqual>b__0'( valuetype [Microsoft.CodeAnalysis.CSharp]Microsoft.CodeAnalysis .
CSharp.SyntaxKind) IL_0015: newobj instance void class [mscorlib]System.Func`2< valuetype [Microsoft.CodeAnalysis.CSharp] Microsoft.CodeAnalysis.CSharp.SyntaxKind,bool>::.
ctor( object, native int) IL_001a: call bool [System.Core]System.Linq.Enumerable::Any< valuetype [Microsoft.CodeAnalysis.CSharp]Microsoft.CodeAnalysis .
CSharp.SyntaxKind>(
class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,
class [mscorlib]System.Func`2<!!0,bool>)
IL_001f: ret
}
Согласитесь, кода здесь немного больше, чем в C#.
Есть два момента, которые хотелось бы сейчас отметить - инструкции по созданию объектов по меткам IL_0000 и IL_0015. В первом случае создается объект типа, автоматически сгенерированного компилятором (то, что лежит «под капотом» лямбда-выражения).
Второй вызов новый объект — создание экземпляра делегата, выполняющего проверку Добрый .
Стоит отметить, что в некоторых случаях компилятор может применить оптимизацию и не добавить инструкцию новый объект для создания экземпляра сгенерированного типа.
Вместо этого он может, например, один раз создать объект, записать его в статическое поле, а затем работать с этим полем.
Такое поведение возникает, например, когда в лямбда-выражении нет захваченных переменных.
Переписанная версия IsKindEqual
Как мы с вами видели, для каждого испытания IsKindEqual «за кулисами» будет создано несколько временных объектов.И, как показала практика (и профилирование), в некоторых случаях это может сыграть существенную роль с точки зрения нагрузки на GC. Один из вариантов — вообще отказаться от использования этого метода.
Как мы видели, вызывающая сторона может просто вызвать метод несколько раз.
Добрый .
Другой вариант — переписать код. Версия «до» выглядит так: public static bool IsKindEqual(this SyntaxNode node, params SyntaxKind[] kinds)
{
return kinds.Any(kind => node.IsKind(kind));
}
Вот как выглядит одна из возможных версий «после»: public static bool IsKindEqual(this SyntaxNode node, params SyntaxKind[] kinds)
{
for (int i = 0; i < kinds.Length; ++i)
{
if (node.IsKind(kinds[i]))
return true;
}
return false;
}
Примечание .
При желании код можно переписать, используя для каждого .
Как мы уже отмечали ранее, когда компилятор знает, что мы определенно работаем с массивом, он все равно будет генерировать код цикла IL. для .
В результате кода стало немного больше, но мы избавились от создания временных объектов, о которых говорилось ранее.
Убедиться в этом можно, посмотрев IL-код — вся инструкция новый объект исчез из него.
.
method public hidebysig static bool IsKindEqual(class Optimizations.SyntaxNode node, valuetype Optimizations.SyntaxKind[] kinds) cil managed { .
custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute:: .
ctor() = ( 01 00 00 00 ) .
param [2] .
custom instance void [mscorlib]System.ParamArrayAttribute:: .
ctor() = ( 01 00 00 00 ) // Code size 29 (0x1d) .
maxstack 3 .
locals init ([0] int32 i)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_0015
IL_0004: ldarg.0
IL_0005: ldarg.1
IL_0006: ldloc.0
IL_0007: ldelem.i4
IL_0008: callvirt instance bool
Optimizations.SyntaxNode::IsKind(valuetype Optimizations.SyntaxKind)
IL_000d: brfalse.s IL_0011
IL_000f: ldc.i4.1
IL_0010: ret
IL_0011: ldloc.0
IL_0012: ldc.i4.1
IL_0013: add
IL_0014: stloc.0
IL_0015: ldloc.0
IL_0016: ldarg.1
IL_0017: ldlen
IL_0018: conv.i4
IL_0019: blt.s IL_0004
IL_001b: ldc.i4.0
IL_001c: ret
}
Переопределение базовых методов в типах значений
Пример кода для семян: enum Origin
{ }
void Foo()
{
Origin origin = default;
while (true)
{
var hashCode = origin.GetHashCode();
}
}
Считаете ли вы, что приведенный выше код нагружает сборщик мусора? Ладно-ладно, учитывая, что код есть в этой статье, ответ очевиден.
Вы поверили этому? На самом деле все не так просто.
Чтобы ответить на вопрос, вам нужно знать, например, работает ли приложение под .
NET Framework или .
NET. Кстати, откуда может быть нагрузка на GC? Кажется, что в управляемой куче не создаются объекты.
Чтобы разобраться в теме, мне пришлось посмотреть IL-код и прочитать спецификацию.
Более подробно я объяснил вопрос в отдельная статья .
В двух словах, вот спойлеры:
- Здесь может произойти перенос вызова метода.
GetHashCode ;
- Чтобы избежать упаковки, переопределите базовые методы в типах значений.
Установка начальной емкости коллекций
Слышал примерно следующее мнение: «Зачем вам задавать начальную емкость коллекции, под капотом все уже оптимизировано».Конечно, что-то оптимизировано (посмотрим, что именно).
Но если говорить о тех частях приложения, где создание почти каждого объекта может влететь в копеечку, то не стоит пренебрегать возможностью сразу сообщить приложению, какой размер коллекции нам понадобится.
Поговорим о том, почему полезно задавать начальную емкость на примере типа Список .
Допустим, у нас есть такой код: static List<Variable> CloneExample(IReadOnlyCollection<Variable> variables)
{
var list = new List<Variable>();
foreach (var variable in variables)
{
list.Add(variable.Clone());
}
return list;
}
Очевидно ли, в чем проблема с этим кодом? Если да, я пожму вам руку.
Если нет, ничего страшного, сейчас разберемся.
Итак, создаем пустой список и постепенно его заполняем.
Соответственно, каждый раз, когда емкость списка заканчивается, нам необходимо:
- выделить память для нового массива, в который будут добавляться элементы списка;
- скопируйте элементы старого массива в новый.
Очевидно, что чем больше размер коллекции переменные , тем большее количество таких операций будет выполнено.
Алгоритм роста списка в нашем случае (для версии .
NET Framework 4.8) будет следующий: 0, 4, 8, 16, 32. То есть, если в коллекции переменные 257 элементов, для этого потребуется создать 8 массивов и 7 операций копирования.
Всех этих дополнительных накладных расходов можно избежать, если сразу задать начальную емкость списка: var list = new List<Variable>(variables.Count);
Эту возможность явно не стоит пренебрегать.
LINQ: разное
Перечисляемый.
Count Метод Перечисляемый.
Count в зависимости от перегрузки может:
- подсчитать количество элементов в коллекции;
- вычислить количество элементов в коллекции, удовлетворяющих предикату.
есть нюанс.
Давайте сначала заглянем внутрь метода.
Исходники, как обычно, возьмем отсюда referencesource.microsoft.com .
Вот как выглядит версия, не принимающая предиката: public static int Count<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
throw Error.ArgumentNull("source");
ICollection<TSource> collectionoft = source as ICollection<TSource>;
if (collectionoft != null)
return collectionoft.Count;
ICollection collection = source as ICollection;
if (collection != null)
return collection.Count;
int count = 0;
using (IEnumerator<TSource> e = source.GetEnumerator())
{
checked
{
while (e.MoveNext())
count++;
}
}
return count;
}
А это версия с предикатом: public static int Count<TSource>(this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
if (source == null)
throw Error.ArgumentNull("source");
if (predicate == null)
throw Error.ArgumentNull("predicate");
int count = 0;
foreach (TSource element in source)
{
checked
{
if (predicate(element))
count++;
}
}
return count;
}
Хорошая новость: беспредикатная версия метода имеет оптимизацию, позволяющую более эффективно рассчитывать количество элементов для коллекций, реализующих ICollection или ICollection .
Однако если это не так, ожидается, что для получения количества элементов будет пройдена вся коллекция.
Это особенно интересно в рамках метода с предикатом.
Допустим, у нас есть следующий код: collection.Count(predicate) > 12;
И в то же время в коллекция — 100 000 элементов.
Вы понимаете, да? Для проверки этого условия нам достаточно найти 13 элементов, для которых предикат (элемент) я бы вернул его истинный .
Однако вместо этого предикат будет применено ко всем 100 000 элементам коллекции.
Особенно обидно становится, если предикат выполняет некоторые относительно тяжелые операции.
Решение есть – велосипед! Я имею в виду, напишите свой аналог/аналоги Считать .
Какую сигнатуру метода использовать (и делать ли это вообще) — решать вам.
Можно просто сделать несколько разных методов, можно сделать метод с хитрой сигнатурой, посредством которого определялось бы какое сравнение нам нужно('> ', '<', '==', etc.).
If suddenly you have identified bottlenecks associated with Считать , но таких мест всего пара — их можно просто переписать на использование цикла для каждого , Например.
Любой -> Количество/Длина
Выше мы уже обсуждали, что вызов метода Любой может стоить нам одного дополнительного итератора.Создание дополнительного объекта можно избежать, используя свойства конкретных коллекций, например Список .
Считать или Массив.
Длина .
Например: static void AnyTest(List<String> values)
{
while (true)
{
// GC
if (values.Any())
// Do smth
// No GC
if (values.Count != 0)
// Do smth
}
}
Этот код менее гибкий, может быть, чуть менее читабельный, но в то же время Может быть позволяют сэкономить на создании итератора.
Да, точно Может быть .
Это зависит от того, вернет ли метод новый объект GetEnumerator .
Более детальное изучение вопроса выявило интересные моменты, которые я могу описать в отдельной заметке.
LINQ -> циклы
Как показала практика, там, где каждый созданный временный объект может ухудшить производительность, имеет смысл отказаться от LINQ в пользу простых циклов.Об этом мы говорили выше на примере Любой И Считать , та же история и с другими методами.
Просто для примера: var strings = collection.OfType<String>()
.
Where(str => str.Length > 62);
foreach (var item in strings)
{
Console.WriteLine(item);
}
Этот код можно переписать, например, так: foreach (var item in collection)
{
if (item is String str && str.Length > 62)
{
Console.WriteLine(str);
}
}
Теги: #C++ #.
NET # Performance #pvs-studio #gc #gc #оптимизации #il #CIL
-
Генерация Трафика
19 Oct, 24 -
7 Золотых Медалей За Дизайн
19 Oct, 24 -
Водонепроницаемая Клавиатура
19 Oct, 24 -
Похоже На Скриншоты Windows 7
19 Oct, 24 -
Дубликаты Появляются В Избранном
19 Oct, 24