【问题标题】:Why is String.IsNullOrEmpty faster than String.Length?为什么 String.IsNullOrEmpty 比 String.Length 快?
【发布时间】:2012-05-08 18:38:15
【问题描述】:

ILSpy 显示String.IsNullOrEmpty 是根据String.Length 实现的。但是为什么String.IsNullOrEmpty(s)s.Length == 0 快​​?

例如,在这个基准测试中它快了 5%:

var stopwatches = Enumerable.Range(0, 4).Select(_ => new Stopwatch()).ToArray();
var strings = "A,B,,C,DE,F,,G,H,,,,I,J,,K,L,MN,OP,Q,R,STU,V,W,X,Y,Z,".Split(',');
var testers = new Func<string, bool>[] { s => s == String.Empty, s => s.Length == 0, s => String.IsNullOrEmpty(s), s => s == "" };
int count = 0;
for (int i = 0; i < 10000; ++i) {
    stopwatches[i % 4].Start();
    for (int j = 0; j < 1000; ++j)
        count += strings.Count(testers[i % 4]);
    stopwatches[i % 4].Stop();
}

(其他基准测试显示类似的结果。这个最小化了 cruft 在我的计算机上运行的影响。另外,与空字符串比较的测试结果相同,比 IsNullOrEmpty 慢约 13%。)

此外,为什么IsNullOrEmpty 仅在 x86 上更快,而在 x64 上String.Length 大约快 9%?

更新: 测试设置详情:.NET 4.0 在 64 位 Windows 7、Intel Core i5 处理器、编译时启用“优化代码”的控制台项目上运行。但是,还启用了“在模块加载时抑制 JIT 优化”(请参阅​​接受的答案和 cmets)。

在完全启用优化的情况下,LengthIsNullOrEmpty 快 14% 左右,移除了委托和其他开销,如本测试所示:

var strings = "A,B,,C,DE,F,,G,H,,,,I,J,,K,L,MN,OP,Q,R,,STU,V,,W,,X,,,Y,,Z,".Split(',');
int count = 0;
for (uint i = 0; i < 100000000; ++i)
    count += strings[i % 32].Length == 0 ? 1 : 0; // Replace Length test with String.IsNullOrEmpty

【问题讨论】:

  • 在不知道您所处的确切情况的情况下,我相当有信心,在大多数情况下,还有其他优化可以解决,而不是检查字符串是否没有任何数据的最快方法在里面。
  • @minitech 因为有 4 个测试人员,而且他是独立计时的。
  • Empty==LengthIsNullOrEmpty 相比如何?
  • 尝试将 s.Length == 0 更改为 Convert.ToBoolean(s.Length) 这会提高性能吗?
  • @Andrew 我同意这种差异并不显着,除非空检查占执行时间的很大比例。我只是好奇,现在我更加好奇了。

标签: c# .net string performance is-empty


【解决方案1】:

这是因为您在 Visual Studio 中运行基准测试,这会阻止 JIT 编译器优化代码。如果不进行优化,则为 String.IsNullOrEmpty

生成此代码
00000000   push        ebp 
00000001   mov         ebp,esp 
00000003   sub         esp,8 
00000006   mov         dword ptr [ebp-8],ecx 
00000009   cmp         dword ptr ds:[00153144h],0 
00000010   je          00000017 
00000012   call        64D85BDF 
00000017   mov         ecx,dword ptr [ebp-8] 
0000001a   call        63EF7C0C 
0000001f   mov         dword ptr [ebp-4],eax 
00000022   movzx       eax,byte ptr [ebp-4] 
00000026   mov         esp,ebp 
00000028   pop         ebp 
00000029   ret 

现在将其与为 Length == 0

生成的代码进行比较
00000000   push   ebp 
00000001   mov    ebp,esp 
00000003   sub    esp,8 
00000006   mov    dword ptr [ebp-8],ecx 
00000009   cmp    dword ptr ds:[001E3144h],0 
00000010   je     00000017 
00000012   call   64C95BDF 
00000017   mov    ecx,dword ptr [ebp-8] 
0000001a   cmp    dword ptr [ecx],ecx 
0000001c   call   64EAA65B 
00000021   mov    dword ptr [ebp-4],eax 
00000024   cmp    dword ptr [ebp-4],0 
00000028   sete   al 
0000002b   movzx  eax,al 
0000002e   mov    esp,ebp 
00000030   pop    ebp 
00000031   ret 

你可以看到,Length == 0 的代码完成了 String.IsNullOrEmpty 的所有代码,但它还尝试了一些类似愚蠢地转换布尔值(返回从长度比较)再次转换为布尔值,这使其比 String.IsNullOrEmpty 慢。

如果您在启用优化的情况下编译程序(发布模式)并直接从 Windows 运行 .exe 文件,则由 JIT 编译器生成的代码要好得多。对于 String.IsNullOrEmpty 它是:

001f0650   push    ebp
001f0651   mov     ebp,esp
001f0653   test    ecx,ecx
001f0655   je      001f0663
001f0657   cmp     dword ptr [ecx+4],0
001f065b   sete    al
001f065e   movzx   eax,al
001f0661   jmp     001f0668
001f0663   mov     eax,1
001f0668   and     eax,0FFh
001f066d   pop     ebp
001f066e   ret

对于长度 == 0

001406f0   cmp     dword ptr [ecx+4],0
001406f4   sete    al
001406f7   movzx   eax,al
001406fa   ret

使用此代码,结果符合预期,即 Length == 0String.IsNullOrEmpty 稍快。

还值得一提的是,在基准测试中使用 Linq、lambda 表达式和计算模并不是一个好主意,因为这些操作很慢(相对于字符串比较而言)并且会使基准测试的结果不准确。

【讨论】:

  • 无论我是在 Visual Studio 内部还是外部运行测试,我都会得到相同的结果。在这两种情况下,我都在针对 .NET Framework 4 的发布模式下构建,并且项目文件中的“优化代码”设置处于启用状态(默认设置)。在 Visual Studio 中,我确实看到了您发布的未优化的汇编代码。如何查看在 Visual Studio 之外运行时生成的汇编代码?
  • 这很奇怪。我在具有不同 CPU 的 3 台具有不同操作系统(Windows server 2008 x64、Windows XP x86)的不同计算机上测试了这个基准测试,我总是认为 Length==0 更快。此外,我在 Visual Studio 中为此项目关闭了 .PDB 文件的生成,但这可能不是问题。你试过另一台电脑吗?我使用WinDbg 附加到运行进程以查看优化的汇编代码。
  • 我重新进行了 VS2010 内部与外部的实验,但无法重现我之前评论中报告的内容。现在我看到的和你一样,Length 在 VS2010 之外时要快一点。我还记得这个设置:工具 > 选项 > 调试 > 常规 > 抑制模块加载时的 JIT 优化。我忘了把它关掉。当我这样做时,我在 VS2010 内外得到相同的结果,Length 更快。此外,通过切换 JIT 优化设置,我可以在 VS2010 中重现所有 4 个汇编代码清单。
  • 这个答案很有趣。那么,这是否意味着.NET 通常不会内联String.IsNullOrEmpty()?这是您提供的对 IsNullOrEmpty() 的优化调用的反汇编。
  • 附加问题,现在写到 2019 年底,if (mystring?.Length &gt; 0)if (!string.IsNullOrEmpty(mystring)) 快吗?
【解决方案2】:

您的基准测试不衡量 String.IsNullOrEmpty 与 String.Length,而是衡量不同的 lambda 表达式如何生成给函数。 IE。仅包含单个函数调用 (IsNullOrEmpty) 的委托比具有函数调用和比较 (长度 == 0) 的委托更快也就不足为奇了。

要比较实际调用 - 编写代码直接调用它们而无需委托。

编辑:我的粗略测量表明,带有 IsNullOrEmpty 的委托版本比其他版本稍快,而在我的机器上直接调用相同比较的顺序是相反的(由于额外代码的数量明显减少,大约快两倍)。结果可能会在机器、x86/x64 模式以及运行时版本之间保持警惕。出于实际目的,如果您需要在 LINQ 查询中使用它们,我会认为所有 4 种方式都差不多。

总的来说,我怀疑这些方法之间的选择在实际程序中会存在可衡量的差异,因此请选择对您来说最易读的一种并使用它。我通常更喜欢 IsNullOrEmpty,因为它减少了在某个条件下出现 ==/!= 错误的机会。

从时间关键代码中完全删除字符串操作可能会带来更大的好处,即在这些选项之间进行选择,也可以选择为关键代码删除 LINQ。与往常一样 - 确保在现实生活场景中测量整体程序速度。

【讨论】:

    【解决方案3】:

    你的测试在某处是错误的。 IsNullOrEmpty 根据定义不能更快,因为它会进行额外的 null 比较操作,然后测试 Length。

    所以答案可能是:因为您的测试,它更快。但是,即使您的代码也显示 IsNullOrEmpty 在我的机器上在 x86 和 x64 模式下始终较慢。

    【讨论】:

    • 我相信 IsNullOrEmpty 在空字符串的情况下可以更快地执行,因为不执行长度检查。尽管我怀疑是否会观察到任何明显的性能提升,但如果字符串通常预期为空,则此检查可能更有意义。
    • 我认为谈论null 字符串的大小写是无效的,因为.Length 在那种情况下根本不适用:)
    【解决方案4】:

    我认为您的测试不正确:

    这个测试表明string.IsNullOrEmpty 总是比s.Length==0 慢,因为它执行了额外的空检查:

    var strings = "A,B,,C,DE,F,,G,H,,,,I,J,,K,L,MN,OP,Q,R,STU,V,W,X,Y,Z,".Split(',');
    var testers = new Func<string, bool>[] { 
        s => s == String.Empty, 
        s => s.Length == 0, 
        s => String.IsNullOrEmpty(s), 
        s => s == "" ,
    };
    int n = testers.Length;
    var stopwatches = Enumerable.Range(0, testers.Length).Select(_ => new Stopwatch()).ToArray();
    int count = 0;
    for(int i = 0; i < n; ++i) { // iterate testers one by one
        Stopwatch sw = stopwatches[i];
        var tester = testers[i];
        sw.Start();
        for(int j = 0; j < 10000000; ++j) // increase this count for better precision
            count += strings.Count(tester);
        sw.Stop();
    }
    for(int i = 0; i < testers.Length; i++)
        Console.WriteLine(stopwatches[i].ElapsedMilliseconds);
    

    结果:

    6573
    5328
    5488
    6419
    

    当您确保目标数据不包含空字符串时,您可以使用s.Length==0。在其他情况下,我建议您使用String.IsNullOrEmpty

    【讨论】:

    • 当我以这种方式构建测试时,我得到相同的结果,但测试之间的标准偏差更高。我认为这是因为其他进程或操作系统代码更容易不公平地影响单个测试人员。平均而言,我仍然以 IsNullOrEmpty 在 x86 上更快。我在 64 位 Core i5 上运行。您是否始终发现 Length 更快?
    【解决方案5】:

    我认为IsNullOrEmpty 不可能更快,因为正如所有其他人所说,它也会检查 null。但无论快不快,差异都会非常小,这给使用IsNullOrEmpty 带来了好处,因为这个额外的空检查使您的代码更安全。

    【讨论】:

      【解决方案6】:

      CLR via CSharp 第 10 章“属性”中,Jeff Richter 写道:

      属性方法可能需要很长时间才能执行;现场访问总是立即完成。使用属性的一个常见原因是执行线程同步,这可以永远停止线程,因此,如果需要线程同步,则不应使用属性。在那种情况下,一种方法是优选的。另外,如果你的类可以远程访问(例如,你的类是从System.MarshalByRefObject派生的),调用属性方法会很慢,因此,方法优先于属性。在我看来,从MarshalByRefObject 派生的类不应该使用属性。

      因此,如果我们看到 String.Length 是属性,而 String.IsNullOrEmpty 是一个可能比属性 String.Length 执行得更快的方法。

      【讨论】:

      • 属性仅作为元数据真正存在。当您“获取”该属性时,它会调用一个名为get_PropertyName 的常规方法,而当您“设置”该属性时,它会调用一个名为set_PropertyName 的常规方法。从 JIT 和执行时间的角度来看,属性和方法没有区别。
      【解决方案7】:

      可能是所涉及的变量的类型引起的。 *Empty 似乎使用布尔值,长度为 int(我猜)。

      和平!

      • :编辑

      【讨论】:

      • -1 用于猜测。并且猜错了。虽然我们肯定鼓励您在此论坛上分享您的知识,但猜测答案会给我们的讨论增加不受欢迎的噪音。
      猜你喜欢
      • 1970-01-01
      • 2019-11-02
      • 1970-01-01
      • 2015-07-24
      • 1970-01-01
      • 2012-12-18
      相关资源
      最近更新 更多