65.9K
CodeProject 正在变化。 阅读更多。
Home

哪个工作更快 - Null 合并运算符、GetValueOrDefault 还是条件运算符

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2015 年 9 月 27 日

Ms-PL

7分钟阅读

viewsIcon

23245

基准测试,哪种方法在获取可空类型的默认值时速度更快——Null 合并运算符、GetValueOrDefault 还是条件运算符。

引言

在我关于 .NET 中 15 个被低估的功能 的文章引起了有趣的讨论。我想知道哪种方法 更快 – ?? (null 合并运算符)、 GetValueOrDefault 方法还是 ?: (条件运算符)。最近我在 Stack Overflow 上读到,大多数人认为 GetValueOrDefault 方法在这三种方法中最快。然而,我决定自己做研究。我无意 进行微优化。我认为在 99% 的情况下,使用这三种方法中的哪一种都不会有影响。通常,你应该选择最容易维护的那种。我不会争论哪种方法更具可读性,因为这是另一个话题。我将向你展示我的研究的基准测试结果。

Null 合并运算符 ??

?? 运算符 在左侧操作数不为 null 时返回左侧操作数,否则返回右侧操作数。可空类型可以包含一个值,也可以是未定义的。?? 运算符 定义了在将可空类型分配给非可空类型时要返回的默认值。

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

条件运算符 ?

条件运算符 (?:) 根据布尔表达式的值返回两个值中的一个。以下是条件运算符的语法。

condition ? first_expression : second_expression;

condition 必须计算为 true 或 false。如果 condition 为 true,则 first_expression 被计算并成为结果。如果 condition 为 false,则 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 合并运算符内部

你可以在以下 URL 上找到 GetValueOrDefault 方法的源代码。该方法有两种重载,一种没有参数,一种需要指定变量为 null 时返回的默认值。

[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
}

Null 合并运算符 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 ?? y 被转换为 x.HasValue ? x.GetValueOrDefault() : y。这应该自动意味着前者很可能比后者快得多。

哪个工作更快 - Null 合并运算符、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();
    }
}

所有测量均在 Release 配置下进行。编写基准测试时使用的正确工具是 System.Diagnostics 命名空间中的 Stopwatch。 (我注意到这个命名空间起得很好;其中的一切对于诊断问题都很有用)。DateTime.Now 是错误的处理方式,它是为解决不同问题而设计的。它比 Stopwatch 难用,并且精度低数千到数百万倍。在 C# 中编写基准测试时,请完全避免使用它。

未能考虑 GC (垃圾回收) 开销会导致你无法测量操作的真实成本,或导致你将成本归咎于错误的代码。在进行基准测试时,尤其是进行比较性基准测试时,我通常的做法是在每次测试之前和之后强制垃圾回收器对所有三个代进行完整回收。

要强制垃圾回收器进行完整回收,请使用以下代码

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. Null 合并运算符设置了默认值
  3. 条件运算符设置了默认值
  4. GetValueOrDefault 未设置默认值
  5. Null 合并运算符返回默认值 0
  6. 条件运算符返回默认值 0

自制基准测试结果

经过多次测试运行后,你可以查看我的研究结果。

NullCoalescing GetValueOrDefault ConditionalOperator   
时间 毫秒 滴答 时间 毫秒 滴答 时间 毫秒 滴答  
6590.08 65900785 12834.06 128340559 2868229.54 28682295356  
6550.64 65506400 12515.2 125152037 2762583.84 27625838427  
6512.58 65125820 12703.32 127033235 2776872.61 27768726079  
6612.16 66121646 13019.02 130190178 2744443.96 27444439560  
6623.67 66236731 12716.64 127166364 2750357.54 27503575389  
6503.09 65030932 12760.48 127604785 2757157.9 27571578966  
6479.25 64792548 12499.89 124998868 2737087.32 27370873195  
6521.75 65217529 12679.25 126792490 2753856.27 27538562686  
6540.68 65406786 12814.12 128141196 2754010.95 27540109549  
6617.96 66179633 13043.48 130434765 2741872.25 27418722520  
6555.186 65551881 12758.546 127585448 2764647.218 27646472173 Average

 

ConditionalOperator Zero GetValueOrDefault Zero NullCoalescing Zero  
时间 毫秒 滴答 时间 毫秒 滴答 时间 毫秒 滴答  
3915.94 39159371 3882.08 38820849 4527.83 45278308  
3890.69 38906893 3853.21 38532131 4493.96 44939560  
3891.92 38919243 3900.99 39009895 4588.62 45886167  
3933.29 39332895 3825.76 38257618 4627.5 46274951  
3880.38 38803838 3824.2 38241964 4624.89 46248852  
4249.6 42496035 4020.71 40207055 4732.86 47328587  
3978.69 39786865 4029.73 40297288 4590.46 45904620  
3964.34 39643393 3966.89 39668937 4769 47689954  
4004.05 40040469 3938.66 39386556 4627.1 46270958  
3862.22 38622233 3882.85 38828455 4521.96 45219593  
3957.112 39571123.5 3912.508 39125074.8 4610.418 46104155 Average

此外,我还创建了两个比较图表以便更好地可视化结果。请记住,我排除了第三个测试用例的结果,因为我不知道为什么,但这个用例比其他的慢得多。

以下是包含所有测试用例平均执行时间的图表。(放大或在新标签页中打开以查看完整尺寸)

以下是包含所有测试用例平均滴答数的图表。(放大或在新标签页中打开以查看完整尺寸)

从我的结果中可以看出,当你想要返回与当前 Nullable 类型默认值不同的默认值时,最佳执行者是 null 合并运算符 (??)。但是,当你想要返回类型的默认值时,GetValueOrDefault 方法会稍快一些。

通过 Telerik JustTrace 进行专业基准测试

我自制的基准测试结果还不够,所以我安装了 Telerik JustTrace (用于 .NET 和原生应用程序的 2 合 1 内存和性能分析器)。相同测试用例的结果略有不同。(放大或在新标签页中打开以查看完整尺寸)

对于返回不同的默认值,GetValueOrDefault 方法比 null 合并运算符 快了 8% 以上。同样,在返回可空类型的默认值的测试用例中,它也再次稍快一些。

C# 系列到目前为止

1. 实现复制粘贴 C# 代码
2. MSBuild TCP IP 日志记录器 C# 代码
3. Windows 注册表读写 C# 代码
4. 运行时更改 .config 文件 C# 代码
5. 通用属性验证器 C# 代码
6. 简化 AutoMapper - 对象映射速度提高 180%
7. C# 6.0 中的 7 个新酷功能
8. 代码覆盖率类型 - C# 示例
9. MSTest 通过 MSTest.exe 包装器应用程序重新运行失败的测试
10. 高效整理 Visual Studio 中 using 语句的技巧
11. 19 个必须知道的 Visual Studio 键盘快捷键 - 第 1 部分
12. 19 个必须知道的 Visual Studio 键盘快捷键 - 第 2 部分
13. 在 Visual Studio 中根据生成配置指定程序集引用
14. .NET 中 15 个被低估的功能
15. .NET 中 15 个被低估的功能(第 2 部分)
16. 在 C# 中轻松格式化货币的技巧
17. 正确断言 DateTime 的 MSTest NUnit C# 代码
18. 哪个工作更快 - Null 合并运算符、GetValueOrDefault 还是条件运算符
19. 基于规范的测试设计技术,用于增强单元测试
20. 使用 Lambda 表达式获取 C# 中的属性名称
21. 使用 C# 的 9 个 Windows 事件日志技巧

 

如果你喜欢我的文章,请随意订阅
另外,请点击这些分享按钮。谢谢!

源代码

参考

 

文章 哪个工作更快 - Null 合并运算符、GetValueOrDefault 还是条件运算符 最先发布于 Automate The Planet

所有图片均从 DepositPhotos.com 购买,不可免费下载和使用。
许可协议

© . All rights reserved.