Статьи

Что быстрее: нулевой оператор объединения против GetValueOrDefault и условный оператор

Моя статья  15 самых недоиспользуемых особенностей .NET  вызвала интересную дискуссию. Мне было интересно узнать, какой метод  быстрее —  ?? (нулевой оператор объединения) метод GetValueOrDefault или  ?: (условный оператор) . Недавно я прочитал в переполнении стека, что большинство людей считают, что метод GetValueOrDefault является самым быстрым среди этих трех. Тем не менее, я решил сделать свое исследование. Я не пытаюсь  микрооптимизировать, Я думаю, что в 99% случаев не будет иметь значения, какой из трех подходов вы собираетесь использовать. Обычно вы должны выбрать тот, который легче поддерживать. Я не собираюсь спорить, какой из них более читабелен, потому что это другая тема. Скорее я собираюсь представить вам результаты моих исследований.

Контрольный нулевой оператор объединения

 

Нулевой оператор объединения

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

int? x = null;
int y = x ?? -1;
Console.WriteLine("y now equals -1 because x was null => {0}", y);
int i = DefaultValueOperatorTest.GetNullableInt() ?? default(int);
Console.WriteLine("i equals now 0 because GetNullableInt() returned null => {0}", i);
string s = DefaultValueOperatorTest.GetStringValue();
Console.WriteLine("Returns 'Unspecified' because s is null => {0}", s ?? "Unspecified");

Официальная документацияhttps://msdn.microsoft.com/en-us/library/ms173224.aspx

Метод GetValueOrDefault

Извлекает значение текущего объекта Nullable <T> или значение объекта по умолчанию. Это быстрее чем ?? оператор.

float? yourSingle = -1.0f;
Console.WriteLine(yourSingle.GetValueOrDefault());
yourSingle = null;
Console.WriteLine(yourSingle.GetValueOrDefault());
// assign different default value
Console.WriteLine(yourSingle.GetValueOrDefault(-2.4f));
// returns the same result as the above statement
Console.WriteLine(yourSingle ?? -2.4f);

Если вы не укажете значение по умолчанию в качестве параметра метода, будет использоваться значение по умолчанию для используемого типа.

Официальная документация:  https://msdn.microsoft.com/en-us/library/72cec0e0(v=vs.110).aspx

Условный оператор?

Условный оператор ( ? 🙂 Возвращает одно из двух значений в зависимости от значения логического выражения. Ниже приведен синтаксис условного оператора.

условие ? first_expression: second_expression;

Условие должно оцениваться как истинное или ложное. Если условие истинно, first_expression оценивается и становится результатом. Если условие ложно, second_expression оценивается и становится результатом. Только одно из двух выражений оценивается.

int input = Convert.ToInt32(Console.ReadLine());
// ?: conditional operator.
string classify = (input > 0) ? "positive" : "negative";

Официальная документация:  https://msdn.microsoft.com/en-us/library/ty67wk28.aspx

GetValueOrDefault и Null Coalescing Оператор Внутренние органы

Вы можете найти исходный код метода GetValueOrDefault по следующему  URL . У метода есть две перегрузки: одна без параметров, а другая требует возврата значения по умолчанию, если переменная равна нулю.

[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault() 
{
    return value;
}

[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault(T defaultValue) 
{
    return hasValue ? value : defaultValue;
}

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

Мне всего этого было недостаточно, поэтому я декомпилировал следующий код, чтобы узнать, как он переводится на общий промежуточный язык (CIL) . Для этой работы я использовал бесплатный  декомпилятор Telerik .NET — Telerik JustDecompile .

public class GetValueOrDefaultAndNullCoalescingOperatorInternals
{
    public void GetValueOrDefaultInternals()
    {
        int? a = null;
        var x = a.GetValueOrDefault(7);
    }

    public void NullCoalescingOperatorInternals()
    {
        int? a = null;
        var x = a ?? 7;
    }
}

GetValueOrDefault CIL

.method public hidebysig instance void GetValueOrDefaultInternals () cil managed 
{
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<int32> a
    )

    IL_0000: ldloca.s a
    IL_0002: initobj valuetype [mscorlib]System.Nullable`1<int32>
    IL_0008: ldloca.s a
    IL_000a: ldc.i4.7
    IL_000b: call instance int32 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault(!0)
    IL_0010: pop
    IL_0011: ret
}

Нулевой оператор коалесцирования CIL

.method public hidebysig instance void NullCoalescingOperatorInternals () cil managed 
{
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<int32> a,
        [1] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000
    )

    IL_0000: ldloca.s a
    IL_0002: initobj valuetype [mscorlib]System.Nullable`1<int32>
    IL_0008: ldloc.0
    IL_0009: stloc.1
    IL_000a: ldloca.s CS$0$0000
    IL_000c: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
    IL_0011: brtrue.s IL_0014

    IL_0013: ret

    IL_0014: ldloca.s CS$0$0000
    IL_0016: call instance int32 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
    IL_001b: pop
    IL_001c: ret
}

Насколько я могу справиться с кодом CIL, я думаю, что х ?? у превращается в x.HasValue? x.GetValueOrDefault (): y. Что автоматически должно означать, что, скорее всего, первое будет намного быстрее, чем позднее.

Который работает быстрее — нулевой оператор объединения, GetValueOrDefault или условный оператор

Для сравнения различных тестовых случаев я создал специализированный класс профилировщика.

public static class Profiler
{
    public static TimeSpan Profile(long iterations, Action actionToProfile)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            actionToProfile();
        }
        watch.Stop();

        return watch.Elapsed;
    }

    public static string FormatProfileResults(long iterations, TimeSpan profileResults)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine(string.Format("Total: {0:0.00} ms ({1:N0} ticks) (over {2:N0} iterations)",
            profileResults.TotalMilliseconds, profileResults.Ticks, iterations));
        var avgElapsedMillisecondsPerRun = profileResults.TotalMilliseconds / (double)iterations;
        var avgElapsedTicksPerRun = profileResults.Ticks / (double)iterations;
        sb.AppendLine(string.Format("AVG: {0:0.00} ms ({1:N0} ticks) (over {2:N0} iterations)",
            avgElapsedMillisecondsPerRun, avgElapsedTicksPerRun, iterations));

        return sb.ToString();
    }
} 

Все датчики выполнены в конфигурации выпуска. Правильный инструмент для написания тестов — секундомер  в  пространстве  имен System.Diagnostics . (Я отмечаю, что это пространство имен хорошо названо; все здесь полезно для диагностики проблем).  DateTime.Now  — неподходящий инструмент для работы, он был разработан, чтобы решить другую проблему. Он сложнее в использовании, чем секундомер, и его точность в тысячи или миллионы раз меньше. Избегайте этого полностью при написании тестов в C #.

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

Чтобы заставить сборщик мусора выполнить полную сборку, используйте следующий код:

GC.Collect();
GC.WaitForPendingFinalizers();

Это шесть контрольных примеров, которые я тестировал.

public static class GetValueOrDefaultVsNullCoalescingOperatorTest
{
    public static void ExecuteWithGetValueOrDefault()
    {
        int? a = null;
        int? b = 3;
        int? d = null;
        int? f = null;
        int? g = null;
        int? h = null;
        int? j = null;
        int? k = 7;

        var profileResult = Profiler.Profile(100000,
            () =>
            {
                var x = a.GetValueOrDefault(7);
                var y = b.GetValueOrDefault(7);
                var z = d.GetValueOrDefault(6) + f.GetValueOrDefault(3) + g.GetValueOrDefault(1) + h.GetValueOrDefault(1) + j.GetValueOrDefault(5) + k.GetValueOrDefault(8);
            });
        string formattedProfileResult = Profiler.FormatProfileResults(100000, profileResult);
        FileWriter.WriteToDesktop("ExecuteWithGetValueOrDefaultT", formattedProfileResult);
    }

    public static void ExecuteWithNullCoalescingOperator()
    {
        int? a = null;
        int? b = 3;
        int? d = null;
        int? f = null;
        int? g = null;
        int? h = null;
        int? j = null;
        int? k = 7;

        var profileResult = Profiler.Profile(100000,
            () =>
            {
                var x = a ?? 7;
                var y = b ?? 7;
                var z = (d ?? 6) + (f ?? 3) + (g ?? 1) + (h ?? 1) + (j ?? 5) + (k ?? 8);
            });
        string formattedProfileResult = Profiler.FormatProfileResults(100000, profileResult);
        FileWriter.WriteToDesktop("ExecuteWithNullCoalescingOperatorT", formattedProfileResult);
    }

    public static void ExecuteWithConditionalOperator()
    {
        int? a = null;
        int? b = 3;
        int? d = null;
        int? f = null;
        int? g = null;
        int? h = null;
        int? j = null;
        int? k = 7;

        var profileResult = Profiler.Profile(100000,
            () =>
            {
                var x = a.HasValue ? a : 7;
                var y = b.HasValue ? b : 7;
                var z = (d.HasValue ? d : 6) + (f.HasValue ? f : 3) + (g.HasValue ? g : 1) + (h.HasValue ? h : 1) + (j.HasValue ? j : 5) + (k.HasValue ? k : 8);
            });
        string formattedProfileResult = Profiler.FormatProfileResults(100000, profileResult);
        FileWriter.WriteToDesktop("ExecuteWithConditionalOperatorT", formattedProfileResult);
    }

    public static void ExecuteWithGetValueOrDefaultZero()
    {
        int? a = null;

        var profileResult = Profiler.Profile(100000,
            () =>
            {
                var x = a.GetValueOrDefault();
            });
        string formattedProfileResult = Profiler.FormatProfileResults(100000, profileResult);
        FileWriter.WriteToDesktop("ExecuteWithGetValueOrDefaultZeroT", formattedProfileResult);
    }

    public static void ExecuteWithNullCoalescingOperatorZero()
    {
        int? a = null;

        var profileResult = Profiler.Profile(100000,
            () =>
            {
                var x = a ?? 0;
            });
        string formattedProfileResult = Profiler.FormatProfileResults(100000, profileResult);
        FileWriter.WriteToDesktop("ExecuteWithNullCoalescingOperatorZeroT", formattedProfileResult);
    }

    public static void ExecuteWithConditionalOperatorZero()
    {
        int? a = null;

        var profileResult = Profiler.Profile(100000,
            () =>
            {
                var x = a.HasValue ? a : 0;
            });
        string formattedProfileResult = Profiler.FormatProfileResults(100000, profileResult);
        FileWriter.WriteToDesktop("ExecuteWithConditionalOperatorZeroT", formattedProfileResult);
    }
}

Выполненные тестовые случаи

(1.) GetValueOrDefault с установленным значением по умолчанию

(2.) Нулевой оператор слияния с установленным значением по умолчанию

(3.) Условный оператор с установленным значением по умолчанию

(4.) GetValueOrDefault без значения по умолчанию

(5.) Нулевой оператор объединения, возвращающий значение по умолчанию 0

(6.) Условный оператор, возвращающий значение по умолчанию 0

Самодельный эталонный инструмент

 

Результаты домашнего теста

После нескольких тестовых прогонов вы можете просмотреть результаты моего исследования.

Результаты теста Null Coalescing Operator

 

Кроме того, я создал две сравнительные таблицы для лучшей визуализации результатов. Имейте в виду, что я исключил результаты для третьего контрольного примера, потому что я не знаю почему, но этот случай был намного медленнее, чем остальные.

Ниже приведена таблица со средним временем выполнения для всех тестовых случаев.

Время выполнения теста Оператор объединения нулей

 

Найдите под диаграммой, содержащей средние значения для всех тестов.

Найдите под диаграммой, содержащей средние такты выполнения для всех тестовых случаев.

 

Как видно из моих результатов, когда вы хотите вернуть значение по умолчанию, отличное от значения по умолчанию для текущего типа Nullable, лучшим исполнителем является оператор объединения  нулей (??) . Однако, когда вы хотите вернуть значение по умолчанию для  метода типа  GetValueOrDefault, это немного быстрее.

Pro Benchmark через Telerik JustTrace

Результаты моего домашнего теста были недостаточны для меня, поэтому я установил  Telerik JustTrace (профилировщик памяти и производительности 2-в-1 для .NET и нативных приложений). Результаты для одних и тех же тестовых случаев немного отличались.

Результаты теста Telerik JustTrace

 

Для возврата другого значения по  умолчанию  метод GetOrDefaultValue был более чем на 8% быстрее, чем  оператор слияния NULL. Кроме того, он снова был немного быстрее среди тестовых случаев, когда возвращается значение по умолчанию для обнуляемого типа.

Пока в серии C #

1.  Реализовать код C Paste Paste

2.  MSBuild TCP IP Logger C # Код

3.  Реестр Windows: чтение, запись, код C #

4.  Измените файл .config во время выполнения кода C #

5.  Общие свойства Validator C # Code

6.  Уменьшенный AutoMapper — Auto-Map объектов на 180% быстрее

7.  7 новых интересных функций в C # 6.0

8.  Типы покрытия кода — примеры в C #

9.  Неудачные тесты MSTest Rerun с помощью программы-оболочки MSTest.exe

10.  Советы по эффективной организации использования в Visual Studio

11.  19 обязательных сочетаний клавиш Visual Studio — часть 1

12.  19 Обязательных сочетаний клавиш Visual Studio — Часть 2

13.  Укажите ссылки на сборки на основе конфигурации сборки в Visual Studio

14.  15 самых малоиспользуемых функций .NET

15.  15 самых малоиспользуемых функций .NET, часть 2

16.  Аккуратные хитрости для легкого форматирования валюты в C #

17.  Утвердите DateTime правильный путь MSTest NUnit C # Code

18.  Что работает быстрее — оператор объединения нулей, GetValueOrDefault или условный оператор

 

Исходный код

Вы можете скачать полный исходный код из моего  репозитория Github .

Если вам понравились мои публикации, не стесняйтесь  ПОДПИСАТЬСЯ 

Кроме того, нажмите эти кнопки обмена. Спасибо! 

Ссылка