为什么 String.IsNullOrEmpty 比 String.Length 快
本文关键字:String Length IsNullOrEmpty 为什么 | 更新日期: 2023-09-27 18:32:26
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%?
更新:测试设置详细信息:.NET 4.0 在 64 位 Windows 7、英特尔酷睿 i5 处理器上运行,在启用"优化代码"的情况下编译的控制台项目。但是,还启用了"在模块加载时抑制 JIT 优化"(请参阅接受的答案和注释)。
完全启用优化后,Length
比删除委托和其他开销的IsNullOrEmpty
快约 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
这是因为您从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 == 0 比 String.IsNullOrEmpty 稍快。
还值得一提的是,在基准测试中使用 Linq、lambda 表达式和计算模并不是一个好主意,因为这些操作很慢(相对于字符串比较),并且使基准测试的结果不准确。
与 String.Length,而是测量函数的不同 lambda 表达式是如何生成的。 也就是说,仅包含单个函数调用(IsNullOrEmpty)的委托比具有函数调用和比较(长度== 0)的委托更快,这并不奇怪。
要比较实际调用 - 编写在没有委托的情况下直接调用它们的代码。
编辑:我的粗略测量表明,使用IsNullOrEmpty的委托版本比其他版本略快,而对同一比较的直接调用在我的机器上以相反的顺序进行(由于额外代码的数量明显减少,因此速度快了大约两倍)。结果可能会在计算机、x86/x64 模式之间以及运行时版本之间保持警惕。出于实际目的,如果您需要在 LINQ 查询中使用它们,我认为所有 4 种方法都大致相同。
总的来说,我怀疑在这些方法之间进行选择的实际程序是否会有可测量的差异,因此请选择最适合您的方法并使用它。我通常更喜欢IsNullOrEmpty,因为它在某种条件下出错的机会较少。
从时间关键代码中完全删除字符串操作将带来更大的好处,在这些选择之间进行选择,也可以为关键代码放弃 LINQ。与往常一样 - 确保在现实生活中测量整体程序速度。
你的测试在某处是错误的。根据定义,IsNullOrEmpty 不能更快,因为它会进行额外的 null 比较操作,然后测试 Length。
所以答案可能是:由于你的测试,它更快。但是,即使你的代码也显示 IsNullOrEmpty 在我的机器上在 x86 和 x64 模式下始终变慢。
你的测试不正确:
此测试表明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
不可能更快,因为正如所有其他人所说,它也检查了 null。但是无论是否更快,差异都会很小,这为使用IsNullOrEmpty
提供了加分项,因为这种额外的空检查使您的代码更安全。
在CLR通过CSharp第10章"属性"中,Jeff Richter写道:
属性方法可能需要很长时间才能执行;字段访问始终立即完成。使用属性的常见原因是执行线程同步,这可能会永久停止线程,因此,如果需要线程同步,则不应使用属性。在这种情况下,首选方法。此外,如果可以远程访问您的类(例如,您的类派生自
System.MarshalByRefObject
),则调用属性方法将非常慢,因此,方法优先于属性。在我看来,从MarshalByRefObject
派生的类永远不应该使用属性。
因此,如果我们看到String.Length
是属性,String.IsNullOrEmpty
是一个可能比属性String.Length
执行得更快的方法。
是由所涉及的变量的类型引起的。*空似乎使用布尔值,长度为整数(我猜)。
和平!
- :编辑