Benchmark StringIsNullOrEmtpy

In todays‘ Twitter post (unfortunately, I lost the link and I don’t find anything in Twitter), I saw the recommendation to use the following pattern.

Don’t use

if (String.IsNullOrEmpty(text))

Use:

if(text is {Length: 0})

Neglecting the style itself, I was heavily interested to understand the runtime impact of this style.

BenchmarkDotNet is quite comfortable here…. But not talking to much about that… Here is the Benchmark Code, just for the pattern:

Static Scenario

[Benchmark()]
public int StringIsLength()
{
    var result = 0;
    for (var n = 0; n < 10000; n++)
    {
        var x = "";
        if (x is { Length: 0 }) { result++; }

        x = "a";
        if (x is { Length: 0 }) { result++; }
    }

    return result;
}

Iteration by 10.000 was needed due to the following result. The IL language looks like the following:

// [49 13 - 49 41] // String.IsNullOrEmpty
IL_001b: call bool [System.Runtime]System.String::IsNullOrEmpty(string)
IL_0020: brfalse.s IL_0026

versus

// [17 13 - 17 36] // x is {Length:0}
IL_000c: ldloc.2 // x
IL_000d: brfalse.s IL_001b
IL_000f: ldloc.2 // x
IL_0010: callvirt instance int32 [System.Runtime]System.String::get_Length()
IL_0015: brtrue.s IL_001b

It heavily now depends on the implementation of get_Length and String.IsNullOrEmpty…

// .Net Code
    public static bool IsNullOrEmpty(string? value)
    {
        return (value == null || 0 == value.Length);
    }

So… looks like x is {Length: 0} can win because it avoids an indirect jump…

The results:

MethodMeanErrorStdDevCode Size
StringIsLength5.800 us0.0650 us0.0508 us65 B
StringIsNullOrEmpty3.131 us0.0077 us0.0068 us17 B
Benchmark Table

StringIsNullOrEmpty is faster and smaller… What happened?

Looking at the disassembly of x is {Length:0}

## .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
```assembly
; Program.StringIsLength()
;         var result = 0;
;         ^^^^^^^^^^^^^^^
       xor       eax,eax
;         for (var n = 0; n < 10000; n++)
;              ^^^^^^^^^
       xor       edx,edx
       mov       rcx,1F848B23020
       mov       rcx,[rcx]
       mov       r8,1F848B2A110
       mov       r8,[r8]
;             var x = "";
;             ^^^^^^^^^^^
M00_L00:
       mov       r9,rcx
;             if (x is { Length: 0 }) { result++; }
;             ^^^^^^^^^^^^^^^^^^^^^^^
       cmp       dword ptr [r9+8],0
       jne       short M00_L01
       inc       eax
;             x = "a";
;             ^^^^^^^^
M00_L01:
       mov       r9,r8
;             if (x is { Length: 0 }) { result++; }
;             ^^^^^^^^^^^^^^^^^^^^^^^
       cmp       dword ptr [r9+8],0
       jne       short M00_L02
       inc       eax
M00_L02:
       inc       edx
       cmp       edx,2710
       jl        short M00_L00
       ret
; Total bytes of code 65

As expected… Now String.IsNullOrEmpty

## .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
```assembly
; Program.StringIsNullOrEmpty()
;         var result = 0;
;         ^^^^^^^^^^^^^^^
       xor       eax,eax
;         for (var n = 0; n < 10000; n++)
;              ^^^^^^^^^
       xor       edx,edx
;                 result++;
;                 ^^^^^^^^^
M00_L00:
       inc       eax
       inc       edx
       cmp       edx,2710
       jl        short M00_L00
       ret
; Total bytes of code 17

??! It just counts from 0 to 10.000 and returns the increase… The jitter just strips of the String.IsNullOrEmpty call and returns the result… It knows, that String.IsNullOrEmpty(„“) is false and String.IsNullOrEmpty(„a“) is true. (Ok, the jitter could also optimize the loop since the result is static)

Conclusion

String.IsNullOrEmpty is faster in static scenarios, now changing to dynamic scenarios. Here, something else will be figured out

Dynamic Scenario

MethodMeanErrorStdDevCode Size
StringIsLengthDynamic6.949 us0.0178 us0.0157 us104 B
StringIsNullOrEmptyDynamic6.942 us0.0229 us0.0215 us104 B
Dynamic Scenario

Same performance!! ? (Code size change is due to some strange stuff caused by BenchmarkDotNet)?

empty = "";
nonEmpty = "a";
[Benchmark()]
public int StringIsLengthDynamic()
{
    var result = 0;
    for (var n = 0; n < 10000; n++)
    {
        if (empty is { Length: 0 }) { result++; }
        if (nonEmpty is { Length: 0 }) { result++; }
    }

    return result;
}

Bäng…

;             if (nonEmpty is { Length: 0 }) { result++; }
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L01:
       mov       rdx,238B4367348
       mov       rdx,[rdx]
       test      rdx,rdx
       je        short M00_L02
       cmp       dword ptr [rdx+8],0
       jne       short M00_L02

vs

;             if (string.IsNullOrEmpty(empty)) { result++; }
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
       mov       rdx,rax
       test      rdx,rdx
       je        short M00_L01
       cmp       dword ptr [rdx+8],0
       jne       short M00_L02,

Conclusio

It does not matter which style to use for performance reasons… It just depends upon which style you prefer. The jitter is aware to optimize String.IsNullOrEmpty.

Here is everything: stringisnullorempty.asm (github.com)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.