1


string.Equals v string.Compare v ToLower/Upper for case insensitive string comparison on .NET 8




Date Added (UTC):

05 Apr 2024 @ 03:57

Date Updated (UTC):

05 Apr 2024 @ 03:57


.NET Version(s):

.NET 8

Tag(s):

#StringComparison #Strings


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using System;

namespace BenchmarkDotNet.Samples
{
    [MemoryDiagnoser]
    [Config(typeof(Config))]
    [SimpleJob(RuntimeMoniker.Net80)]
    [HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
    public class StringComparisonBenchmarks
    {
        private class Config : ManualConfig
        {
            public Config()
            {
                SummaryStyle =
                    SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
            }
        }

        private string str1 = "HELLO WORLD";
        private string str2 = "hello world";

        [Benchmark(Baseline = true)]
        public bool Equals_OrdinalIgnoreCase() => string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase);

        [Benchmark]
        public bool Compare_OrdinalIgnoreCase() => string.Compare(str1, str2, StringComparison.OrdinalIgnoreCase) == 0;

        [Benchmark]
        public bool ToLower() => str1.ToLower() == str2.ToLower();

        [Benchmark]
        public bool ToUpper() => str1.ToUpper() == str2.ToUpper();
    }
}

// .NET 8
public bool Equals_OrdinalIgnoreCase()
{
    return string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase);
}
// .NET 8
public bool Compare_OrdinalIgnoreCase()
{
    return string.Compare(str1, str2, StringComparison.OrdinalIgnoreCase) == 0;
}
// .NET 8
public bool ToLower()
{
    return str1.ToLower() == str2.ToLower();
}
// .NET 8
public bool ToUpper()
{
    return str1.ToUpper() == str2.ToUpper();
}

// .NET 8
.method public hidebysig 
    instance bool Equals_OrdinalIgnoreCase () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 01 00 54 02 08 42 61 73
        65 6c 69 6e 65 01
    )
    // Method begins at RVA 0x2050
    // Code size 19 (0x13)
    .maxstack 8

    // sequence point: (line 40, col 51) to (line 40, col 112) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str1
    IL_0006: ldarg.0
    IL_0007: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str2
    IL_000c: ldc.i4.5
    IL_000d: call bool [System.Runtime]System.String::Equals(string, string, valuetype [System.Runtime]System.StringComparison)
    IL_0012: ret
}
// .NET 8
.method public hidebysig 
    instance bool Compare_OrdinalIgnoreCase () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2064
    // Code size 22 (0x16)
    .maxstack 8

    // sequence point: (line 43, col 52) to (line 43, col 119) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str1
    IL_0006: ldarg.0
    IL_0007: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str2
    IL_000c: ldc.i4.5
    IL_000d: call int32 [System.Runtime]System.String::Compare(string, string, valuetype [System.Runtime]System.StringComparison)
    IL_0012: ldc.i4.0
    IL_0013: ceq
    IL_0015: ret
}
// .NET 8
.method public hidebysig 
    instance bool ToLower () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2d 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x207b
    // Code size 28 (0x1c)
    .maxstack 8

    // sequence point: (line 46, col 34) to (line 46, col 66) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str1
    IL_0006: callvirt instance string [System.Runtime]System.String::ToLower()
    IL_000b: ldarg.0
    IL_000c: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str2
    IL_0011: callvirt instance string [System.Runtime]System.String::ToLower()
    IL_0016: call bool [System.Runtime]System.String::op_Equality(string, string)
    IL_001b: ret
}
// .NET 8
.method public hidebysig 
    instance bool ToUpper () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 30 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2098
    // Code size 28 (0x1c)
    .maxstack 8

    // sequence point: (line 49, col 34) to (line 49, col 66) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str1
    IL_0006: callvirt instance string [System.Runtime]System.String::ToUpper()
    IL_000b: ldarg.0
    IL_000c: ldfld string BenchmarkDotNet.Samples.StringComparisonBenchmarks::str2
    IL_0011: callvirt instance string [System.Runtime]System.String::ToUpper()
    IL_0016: call bool [System.Runtime]System.String::op_Equality(string, string)
    IL_001b: ret
}

// .NET 8 (X64)
Equals_OrdinalIgnoreCase()
    L0000: mov rdx, rcx
    L0003: mov rcx, [rdx+8]
    L0007: mov rdx, [rdx+0x10]
    L000b: mov r8d, 5
    L0011: jmp qword ptr [0x7ffa9bc361d8]
// .NET 8 (X64)
Compare_OrdinalIgnoreCase()
    L0000: sub rsp, 0x28
    L0004: mov rdx, rcx
    L0007: mov rcx, [rdx+8]
    L000b: mov rdx, [rdx+0x10]
    L000f: mov r8d, 5
    L0015: call qword ptr [0x7ffa9bc35fe0]
    L001b: test eax, eax
    L001d: sete al
    L0020: movzx eax, al
    L0023: add rsp, 0x28
    L0027: ret
// .NET 8 (X64)
ToLower()
    L0000: push rdi
    L0001: push rsi
    L0002: push rbp
    L0003: push rbx
    L0004: sub rsp, 0x28
    L0008: mov rbx, rcx
    L000b: mov rsi, [rbx+8]
    L000f: cmp [rsi], sil
    L0012: call qword ptr [0x7ffa9bd3e1c0]
    L0018: mov rdi, [rax]
    L001b: mov rbp, 0x7ffa9bd422d0
    L0025: cmp rdi, rbp
    L0028: jne short L007e
    L002a: mov rcx, rax
    L002d: call qword ptr [0x7ffa9bd423c8]
    L0033: mov rcx, rax
    L0036: mov rdx, rsi
    L0039: cmp [rcx], ecx
    L003b: call qword ptr [0x7ffa9bdfefb8]
    L0041: mov rsi, rax
    L0044: mov rbx, [rbx+0x10]
    L0048: cmp [rbx], bl
    L004a: call qword ptr [0x7ffa9bd3e1c0]
    L0050: mov rdi, [rax]
    L0053: cmp rdi, rbp
    L0056: jne short L00a9
    L0058: mov rcx, rax
    L005b: call qword ptr [0x7ffa9bd423c8]
    L0061: mov rcx, rax
    L0064: mov rdx, rbx
    L0067: cmp [rcx], ecx
    L0069: call qword ptr [0x7ffa9bdfefb8]
    L006f: mov rdx, rax
    L0072: cmp rsi, rdx
    L0075: jne short L00b5
    L0077: mov eax, 1
    L007c: jmp short L009f
    L007e: mov rcx, rax
    L0081: mov rax, [rdi+0x48]
    L0085: call qword ptr [rax+0x38]
    L0088: jmp short L0033
    L008a: lea rcx, [rsi+0xc]
    L008e: mov r8d, [rsi+8]
    L0092: add r8d, r8d
    L0095: add rdx, 0xc
    L0099: call qword ptr [0x7ffa9bce5068]
    L009f: nop
    L00a0: add rsp, 0x28
    L00a4: pop rbx
    L00a5: pop rbp
    L00a6: pop rsi
    L00a7: pop rdi
    L00a8: ret
    L00a9: mov rcx, rax
    L00ac: mov rax, [rdi+0x48]
    L00b0: call qword ptr [rax+0x38]
    L00b3: jmp short L0061
    L00b5: test rsi, rsi
    L00b8: je short L00c7
    L00ba: test rdx, rdx
    L00bd: je short L00c7
    L00bf: mov eax, [rsi+8]
    L00c2: cmp eax, [rdx+8]
    L00c5: je short L008a
    L00c7: xor eax, eax
    L00c9: jmp short L009f
// .NET 8 (X64)
ToUpper()
    L0000: push rsi
    L0001: push rbx
    L0002: sub rsp, 0x28
    L0006: mov rbx, rcx
    L0009: mov rsi, [rbx+8]
    L000d: cmp [rsi], sil
    L0010: call qword ptr [0x7ffa9bd3e1c0]
    L0016: mov rcx, rax
    L0019: mov rax, [rax]
    L001c: mov rax, [rax+0x48]
    L0020: call qword ptr [rax+0x38]
    L0023: mov rcx, rax
    L0026: mov rdx, rsi
    L0029: cmp [rcx], ecx
    L002b: call qword ptr [0x7ffa9bdff0a8]
    L0031: mov rsi, rax
    L0034: mov rbx, [rbx+0x10]
    L0038: cmp [rbx], bl
    L003a: call qword ptr [0x7ffa9bd3e1c0]
    L0040: mov rcx, rax
    L0043: mov rax, [rax]
    L0046: mov rax, [rax+0x48]
    L004a: call qword ptr [rax+0x38]
    L004d: mov rcx, rax
    L0050: mov rdx, rbx
    L0053: cmp [rcx], ecx
    L0055: call qword ptr [0x7ffa9bdff0a8]
    L005b: mov rdx, rax
    L005e: cmp rsi, rdx
    L0061: jne short L006a
    L0063: mov eax, 1
    L0068: jmp short L0095
    L006a: test rsi, rsi
    L006d: je short L007c
    L006f: test rdx, rdx
    L0072: je short L007c
    L0074: mov eax, [rsi+8]
    L0077: cmp eax, [rdx+8]
    L007a: je short L0080
    L007c: xor eax, eax
    L007e: jmp short L0095
    L0080: lea rcx, [rsi+0xc]
    L0084: mov r8d, [rsi+8]
    L0088: add r8d, r8d
    L008b: add rdx, 0xc
    L008f: call qword ptr [0x7ffa9bce5068]
    L0095: nop
    L0096: add rsp, 0x28
    L009a: pop rbx
    L009b: pop rsi
    L009c: ret


Benchmark Description:


string.Equals doesn't need to create strings like ToUpper/ToLower. In this case the strings match, but if they didn't the perf didn't would be even larger as string.Equals ***can exit early*** upon the first difference unlike ToUpper/ToLower.

The provided benchmark code is designed to measure and compare the performance of different methods for case-insensitive string comparison in .NET. This benchmark is set up using BenchmarkDotNet, a powerful .NET library for benchmarking code performance. The configuration and specific benchmarks are designed to provide insights into the efficiency and resource usage of various string comparison techniques. Here's an overview of the general setup and the rationale behind each benchmark method: ### General Setup - **.NET Version**: The benchmark specifies `[SimpleJob(RuntimeMoniker.Net80)]`, indicating that it targets .NET 8.0. This is important as performance characteristics can vary between different .NET runtime versions due to optimizations and changes in the framework. - **MemoryDiagnoser**: Enabled by `[MemoryDiagnoser]`, this feature collects and reports memory allocation data, which is crucial for understanding the memory efficiency of each method. - **Config**: The custom configuration class `Config` modifies the summary style to display ratios in percentage format, making the results easier to interpret. - **HideColumns**: Certain columns are hidden (`Column.Job`, `Column.RatioSD`, `Column.AllocRatio`) to focus on the most relevant data for this benchmark. ### Benchmark Methods #### 1. `Equals_OrdinalIgnoreCase` - **Purpose**: Measures the performance of case-insensitive string comparison using `string.Equals` with `StringComparison.OrdinalIgnoreCase`. - **Rationale**: This method is commonly recommended for case-insensitive comparisons because it's optimized for performance and correctly handles cultural differences without converting the case of the strings. - **Expected Insights**: This benchmark is expected to show good performance with minimal memory allocations, serving as a baseline for comparison with other methods. #### 2. `Compare_OrdinalIgnoreCase` - **Purpose**: Tests the performance of using `string.Compare` with `StringComparison.OrdinalIgnoreCase` for case-insensitive string comparison, checking for equality by comparing the result to 0. - **Rationale**: Similar to `Equals_OrdinalIgnoreCase`, but uses a different API that might have different performance characteristics. It's important to compare both to understand which is more efficient for equality checks. - **Expected Insights**: Likely to have performance close to `Equals_OrdinalIgnoreCase`, but the comparison might introduce slight overhead. Memory usage should be comparable. #### 3. `ToLower` - **Purpose**: Evaluates the performance of converting both strings to lowercase using `ToLower` and then comparing them for equality. - **Rationale**: This approach is sometimes used for case-insensitive comparison but can be less efficient due to the creation of new string instances and potential cultural differences in case conversion. - **Expected Insights**: This method is expected to show higher memory allocations due to the creation of new strings and potentially slower performance compared to ordinal comparison methods. #### 4. `ToUpper` - **Purpose**: Similar to `ToLower`, this benchmark measures the performance of converting both strings to uppercase using `ToUpper` before comparing them. - **Rationale**: This is another common technique for case-insensitive comparison. It's useful to compare its performance and memory usage against `ToLower` and ordinal methods. - **Expected Insights**: Like `ToLower`, higher memory allocations and possibly slower performance are expected due to the creation of new strings. The choice between `ToUpper` and `ToLower` might also be influenced by specific cultural or linguistic considerations. ### Conclusion Running these benchmarks provides insights into the efficiency (both in terms of execution time and memory usage) of different strategies for case-insensitive string comparison in .NET. This can guide developers in choosing the most appropriate method based on their specific requirements, balancing performance and correctness (e.g., handling of cultural differences).


Benchmark Comments: