|
using BenchmarkDotNet.Attributes; |
|
using BenchmarkDotNet.Running; |
|
using System.Diagnostics.CodeAnalysis; |
|
using System.Text; |
|
|
|
var summary = BenchmarkRunner.Run<Lookups>(); |
|
|
|
[MemoryDiagnoser] |
|
public class Lookups |
|
{ |
|
static readonly HashSet<string> CustomLookup = new HashSet<string>(new OurStringComparer()) |
|
{ |
|
"abcdefgh", |
|
"bcdefghi", |
|
"cdefghij", |
|
"defghijk", |
|
"efghijkl", |
|
"fghijklm", |
|
"ghijklmn", |
|
"hijklmno", |
|
}; |
|
|
|
static readonly HashSet<string> DotnetLookup = new HashSet<string>(StringComparer.Ordinal) |
|
{ |
|
"abcdefgh", |
|
"bcdefghi", |
|
"cdefghij", |
|
"defghijk", |
|
"efghijkl", |
|
"fghijklm", |
|
"ghijklmn", |
|
"hijklmno", |
|
}; |
|
|
|
private static string ExistingKeyString => "efghijkl"; |
|
private static ReadOnlySpan<char> ExistingKeyUtf16 => "efghijkl"; |
|
private static ReadOnlySpan<byte> ExistingKeyUtf8 => "efghijkl"u8; |
|
|
|
private static string NonExistingKeyString => "stuvwxyz"; |
|
private static ReadOnlySpan<char> NonExistingKeyUtf16 => "stuvwxyz"; |
|
private static ReadOnlySpan<byte> NonExistingKeyUtf8 => "stuvwxyz"u8; |
|
|
|
[Benchmark(Baseline = true)] |
|
public bool Dotnet_String_Hit() => DotnetLookup.Contains(ExistingKeyString); |
|
|
|
[Benchmark] |
|
public bool Dotnet_String_Miss() => DotnetLookup.Contains(NonExistingKeyString); |
|
|
|
[Benchmark] |
|
public bool Dotnet_Span_Hit() |
|
{ |
|
var spanLookup = DotnetLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(ExistingKeyUtf16); |
|
} |
|
|
|
[Benchmark] |
|
public bool Dotnet_Span_Miss() |
|
{ |
|
var spanLookup = DotnetLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(NonExistingKeyUtf16); |
|
} |
|
|
|
[Benchmark] |
|
public bool Dotnet_Utf8Decode_Hit() |
|
{ |
|
Span<char> decoded = stackalloc char[ExistingKeyUtf8.Length]; |
|
decoded = decoded[..Encoding.UTF8.GetChars(ExistingKeyUtf8, decoded)]; |
|
var spanLookup = DotnetLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(decoded); |
|
} |
|
|
|
[Benchmark] |
|
public bool Dotnet_Utf8Decode_Miss() |
|
{ |
|
Span<char> decoded = stackalloc char[NonExistingKeyUtf8.Length]; |
|
decoded = decoded[..Encoding.UTF8.GetChars(NonExistingKeyUtf8, decoded)]; |
|
var spanLookup = DotnetLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(decoded); |
|
} |
|
|
|
[Benchmark] |
|
public bool Custom_String_Hit() => CustomLookup.Contains(ExistingKeyString); |
|
|
|
[Benchmark] |
|
public bool Custom_String_Miss() => CustomLookup.Contains(NonExistingKeyString); |
|
|
|
[Benchmark] |
|
public bool Custom_Span_Hit() |
|
{ |
|
var spanLookup = CustomLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(ExistingKeyUtf16); |
|
} |
|
|
|
[Benchmark] |
|
public bool Custom_Span_Miss() |
|
{ |
|
var spanLookup = CustomLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(NonExistingKeyUtf16); |
|
} |
|
|
|
[Benchmark] |
|
public bool Custom_Utf8_Hit() |
|
{ |
|
var utf8SpanLookup = CustomLookup.GetAlternateLookup<string, ReadOnlySpan<byte>>(); |
|
return utf8SpanLookup.Contains(ExistingKeyUtf8); |
|
} |
|
|
|
[Benchmark] |
|
public bool Custom_Utf8_Miss() |
|
{ |
|
var utf8SpanLookup = CustomLookup.GetAlternateLookup<string, ReadOnlySpan<byte>>(); |
|
return utf8SpanLookup.Contains(NonExistingKeyUtf8); |
|
} |
|
|
|
[Benchmark] |
|
public bool Custom_Utf8Decode_Hit() |
|
{ |
|
Span<char> decoded = stackalloc char[ExistingKeyUtf8.Length]; |
|
decoded = decoded[..Encoding.UTF8.GetChars(ExistingKeyUtf8, decoded)]; |
|
var spanLookup = CustomLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(decoded); |
|
} |
|
|
|
[Benchmark] |
|
public bool Custom_Utf8Decode_Miss() |
|
{ |
|
Span<char> decoded = stackalloc char[NonExistingKeyUtf8.Length]; |
|
decoded = decoded[..Encoding.UTF8.GetChars(NonExistingKeyUtf8, decoded)]; |
|
var spanLookup = CustomLookup.GetAlternateLookup<string, ReadOnlySpan<char>>(); |
|
return spanLookup.Contains(decoded); |
|
} |
|
} |
|
|
|
public ref struct Utf8SpanRuneEnumerator |
|
{ |
|
private ReadOnlySpan<byte> _remaining; |
|
private Rune _current; |
|
|
|
public Utf8SpanRuneEnumerator(ReadOnlySpan<byte> buffer) |
|
{ |
|
_remaining = buffer; |
|
_current = default; |
|
} |
|
|
|
public Rune Current => _current; |
|
|
|
public Utf8SpanRuneEnumerator GetEnumerator() => this; |
|
|
|
public bool MoveNext() |
|
{ |
|
if (_remaining.IsEmpty) |
|
{ |
|
_current = default; |
|
return false; |
|
} |
|
|
|
Rune.DecodeFromUtf8(_remaining, out _current, out var bytesConsumed); |
|
_remaining = _remaining.Slice(bytesConsumed); |
|
return true; |
|
} |
|
} |
|
|
|
class OurStringComparer : IEqualityComparer<string>, |
|
IAlternateEqualityComparer<ReadOnlySpan<char>, string>, |
|
IAlternateEqualityComparer<ReadOnlySpan<byte>, string> |
|
{ |
|
public string Create(ReadOnlySpan<char> alternate) => new(alternate); |
|
|
|
public bool Equals(string? x, string? y) => string.Equals(x, y); |
|
|
|
public bool Equals(ReadOnlySpan<char> alternate, string other) => |
|
other?.AsSpan().SequenceEqual(alternate) ?? false; |
|
|
|
public bool Equals(ReadOnlySpan<byte> alternate, string other) |
|
{ |
|
var utf16Enumerator = other.EnumerateRunes(); |
|
var utf8Enumerator = new Utf8SpanRuneEnumerator(alternate); |
|
while (utf16Enumerator.MoveNext()) |
|
{ |
|
if (!utf8Enumerator.MoveNext()) |
|
return false; |
|
if (utf16Enumerator.Current != utf8Enumerator.Current) |
|
return false; |
|
} |
|
return !utf8Enumerator.MoveNext(); |
|
} |
|
|
|
public int GetHashCode([DisallowNull] string str) => |
|
str is null ? 0 : GetHashCode(str.AsSpan()); |
|
|
|
public string Create(ReadOnlySpan<byte> alternate) => Encoding.UTF8.GetString(alternate); |
|
|
|
public int GetHashCode(ReadOnlySpan<char> alternate) |
|
{ |
|
uint hash = 5381; |
|
foreach (var rune in alternate.EnumerateRunes()) |
|
hash = hash * 33u + (uint)rune.Value; |
|
return (int)hash; |
|
} |
|
|
|
public int GetHashCode(ReadOnlySpan<byte> alternate) |
|
{ |
|
uint hash = 5381; |
|
foreach (var rune in new Utf8SpanRuneEnumerator(alternate)) |
|
hash = hash * 33u + (uint)rune.Value; |
|
return (int)hash; |
|
} |
|
} |