为什么String.IsNullOrEmpty比String.Length更快?

25

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

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

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

更新: 测试设置详情: 在64位Windows 7操作系统上运行.NET 4.0,使用英特尔Core i5处理器,编译时启用"优化代码"选项。但是,“在模块加载时禁止JIT优化”也已启用(请参见接受的答案和评论)。

在完全启用优化的情况下,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

8
在不了解你所处的确切情况之前,我相当有信心,在大多数情况下,除了检查字符串是否为空之外,还有其他优化方案可供考虑。 - Andrew
@minitech 因为有4个测试员,而他们是独立计时的。 - Adam Liss
3
我同意Andrew所说的,除非空检查在执行时间中占据了不寻常的大比例,否则差异并不显著。我只是好奇,现在我更加好奇了。 - Edward Brey
所有微观优化一样,错误几乎肯定在测试代码中,你没有正确地对代码进行基准测试。有效的微基准测试很难,你可以通过表面上看起来是公平比较的代码轻松地获得任何想要的结果。 - Servy
@Servy: 你说得对。错误通常出现在基准测试代码或(正如在这种情况下)执行环境中。然而,有时候也会出现真正的微小优化问题,有时候你最不希望它们出现。例如,在调查这个问题时,我偶然发现了一种情况,即添加本地变量会使.NET代码变慢,这似乎是一个真正的编译器错误。 - Edward Brey
显示剩余4条评论
7个回答

26

这是因为你在 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

对于 Length == 0 的情况:

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

使用这段代码,结果如预期,即Length == 0String.IsNullOrEmpty略快。

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


无论我是在 Visual Studio 内还是外部运行测试,我都会得到相同的结果。在这两种情况下,我都是使用 Release 模式构建 .NET Framework 4,并且项目文件中的“优化代码”设置已打开(默认设置)。在 Visual Studio 中,我确实看到了您发布的未经优化的汇编代码。那么,在 Visual Studio 外部运行时,您如何查看生成的汇编代码呢? - Edward Brey
很奇怪。我在3台不同的计算机上测试了这个基准,它们有不同的操作系统(Windows server 2008 x64,Windows XP x86),不同的CPU,但我总是得到Length==0更快的结果。此外,我为该项目关闭了Visual Studio中的.PDB文件生成,但这可能不是问题所在。你试过另一台电脑吗?我使用WinDbg附加到正在运行的进程,以查看优化后的汇编代码。 - Ňuf
我重新运行了内部VS2010和外部VS2010的实验,但无法复制我在之前评论中所报告的内容。现在我看到与您相同的情况,即当在VS2010之外时,“Length”较快。我还记得这个设置:工具>选项>调试>常规>抑制模块加载时的JIT优化。我忘记关闭它了。当我这样做时,在VS2010内外,我得到相同的结果,“Length”更快。另外,通过切换那个JIT优化设置,我可以在VS2010中复制你所有的四个汇编代码列表。 - Edward Brey
这个答案非常有趣。那么,这是否意味着.NET通常不会内联String.IsNullOrEmpty()呢?从你提供的对IsNullOrEmpty()优化调用的反汇编结果来看,似乎是这样的。 - reirab
额外的问题,现在是2019年底,if (mystring?.Length > 0)if (!string.IsNullOrEmpty(mystring))更快吗? - Thomas Williams

4
你的基准测试并没有比较String.IsNullOrEmpty和String.Length,而是比较了不同lambda表达式生成函数时的性能差异。也就是说,只包含单个函数调用(IsNullOrEmpty)的委托要比包含函数调用和比较操作(Length == 0)的委托更快,这并不奇怪。
如果想要比较实际调用的性能,请直接编写调用它们的代码,而不是使用委托。
编辑:我的初步测量显示,只包含IsNullOrEmpty的委托版本略微比其余版本更快,而直接调用相同比较的结果则相反(由于额外代码数量显著较少,速度约为两倍)。在不同机器、x86/x64模式以及运行时版本之间,结果可能会有所不同。对于LINQ查询,我认为这4种方式的性能大致相同。
总体而言,我怀疑在选择这些方法之间对于真正的程序效果会有明显的差异,因此选择最易读的方法并使用它即可。我通常更喜欢IsNullOrEmpty,因为它在条件语句中出错的可能性更小。
从时间关键代码中完全删除字符串操作可能带来更高的收益,丢弃LINQ也是一个选择。无论如何,请确保在实际情况下测量整个程序的速度。

1

我认为你的测试结果不正确:

这个测试表明,string.IsNullOrEmptys.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


当我以这种方式组织测试时,我得到相同的结果,但是测试之间的标准差更高。我认为这是因为其他进程或操作系统代码更容易不公平地影响单个测试器。平均而言,在x86上,我的结果仍然表明 IsNullOrEmpty 更快。我在64位Core i5上运行。你是否始终发现 Length 更快? - Edward Brey

1

你的测试结果有误。根据定义,IsNullOrEmpty不可能更快,因为它需要进行额外的空值比较操作,然后才能测试长度。

所以答案可能是:由于你的测试方法不同,因此它更快。然而,即使使用你的代码,在我的机器上无论是在x86还是x64模式下,IsNullOrEmpty始终都比较慢。


我相信在空字符串的情况下,IsNullOrEmpty函数可以更快地执行,因为它不需要进行长度检查。虽然我怀疑在性能方面会有任何显著的提升,但如果该字符串经常被期望为空,则这种检查可能更有意义。 - overslacked
1
我认为谈论“null”字符串的情况是无效的,因为在这种情况下根本不适用.Length :) - Petr Abdulin

0

我认为 IsNullOrEmpty 不可能更快,因为正如其他人所说,它也会检查 null。但无论快慢,差异都将非常小,这使得使用 IsNullOrEmpty 更加安全,因为它提供了额外的 null 检查。


-2
CLR via CSharp第10章“属性”中,Jeff Richter写道:
属性方法可能需要很长时间才能执行;字段访问总是立即完成。使用属性的常见原因是执行线程同步,这可能会永久停止线程,因此,如果需要线程同步,则不应使用属性。在这种情况下,最好使用方法。此外,如果可以远程访问您的类(例如,您的类派生自System.MarshalByRefObject),则调用属性方法将非常缓慢,因此,方法比属性更受欢迎。在我看来,从MarshalByRefObject派生的类永远不应使用属性。
因此,如果我们看到String.Length是属性,而String.IsNullOrEmpty是一个方法,它可能比属性String.Length执行得更快。

1
一个属性实际上只存在于元数据中。当你“获取”属性时,它调用一个名为 get_PropertyName 的常规方法,当你“设置”属性时,它调用一个名为 set_PropertyName 的常规方法。从JIT和执行时间的角度来看,属性和方法之间没有区别。 - Sam Harwell

-4

这可能是由于涉及变量的类型不同引起的。 * Empty 似乎使用布尔值,而 Length 使用整数(我猜测)。

祝好!

  • : 编辑

3
-1 - Bob Kaufman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接