Skip to content

Commit

Permalink
Add a zero-allocation version of the API
Browse files Browse the repository at this point in the history
  • Loading branch information
atifaziz authored and alexmg committed Sep 3, 2024
1 parent ed199a4 commit 835d13f
Show file tree
Hide file tree
Showing 8 changed files with 391 additions and 59 deletions.
106 changes: 106 additions & 0 deletions src/Moniker/Chars.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Text;

namespace Moniker;

/// <summary>
/// Represents a reference to a string of characters.
/// </summary>
public readonly ref struct Chars
{
private readonly ReadOnlySpan<byte> _u8str;

internal Chars(ReadOnlySpan<byte> u8str, int charCount)
{
_u8str = u8str;
Length = charCount;
}

/// <summary>
/// The length in characters.
/// </summary>
public int Length { get; }

/// <summary>
/// The length when encoded in UTF-8 bytes.
/// </summary>
public int Utf8Length => _u8str.Length;

/// <summary>
/// Writes characters to the specified buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <returns>The number of characters written.</returns>
public int Write(Span<char> buffer)
{
var count = Length;
if (count > buffer.Length)
return 0;

Encoding.UTF8.GetChars(_u8str, buffer);
return count;
}

/// <summary>
/// Writes character in UTF-8 encoding to the specified buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <returns>The number of characters written.</returns>
public int WriteUtf8(Span<byte> buffer) =>
_u8str.TryCopyTo(buffer) ? Utf8Length : 0;

/// <summary>
/// Returns a string with the same characters.
/// </summary>
public override string ToString()
{
var chars = Length <= 64 ? stackalloc char[Length] : new char[Length];
_ = Write(chars);
return new(chars);
}

#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member

/// <summary>
/// This method is unsupported and throws <see cref="NotSupportedException"/>
/// because <seealso cref="Chars"/> instances cannot be boxed. Use
/// <see cref="op_Equality"/> instead.
/// </summary>
/// <exception cref="NotSupportedException">Always thrown.</exception>
[Obsolete("This method is unsupported and will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj) => throw new NotSupportedException();

/// <summary>
/// This method is unsupported and throws <see cref="NotSupportedException"/>.
/// </summary>
/// <exception cref="NotSupportedException">Always thrown.</exception>
[Obsolete("This method is unsupported and will always throw an exception.")]
public override int GetHashCode() => throw new NotSupportedException();

#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member

/// <summary>
/// Compares to another <see cref="Chars"/> for equality.
/// </summary>
/// <param name="other">The other <see cref="Chars"/> to compare to.</param>
/// <returns><see langword="true" /> if this instance compares equals to <paramref name="other"/>.</returns>
public bool Equals(Chars other) => _u8str.SequenceEqual(other._u8str);

internal bool Equals(ReadOnlySpan<byte> other) => _u8str.SequenceEqual(other);

/// <summary>
/// Compares two <see cref="Chars"/> for equality.
/// </summary>
/// <param name="left">First operand to compare.</param>
/// <param name="right">Second operand to compare.</param>
/// <returns><see langword="true" /> if <paramref name="left"/> equals <paramref name="right"/>.</returns>
public static bool operator ==(Chars left, Chars right) => left.Equals(right);

/// <summary>
/// Compares two <see cref="Chars"/> for inequality.
/// </summary>
/// <param name="left">First operand to compare.</param>
/// <param name="right">Second operand to compare.</param>
/// <returns><see langword="true" /> if <paramref name="left"/> does not equal <paramref name="right"/>.</returns>
public static bool operator !=(Chars left, Chars right) => !left.Equals(right);
}
90 changes: 66 additions & 24 deletions src/Moniker/NameGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Moniker
{
Expand All @@ -21,12 +23,27 @@ public static class NameGenerator
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static string Generate(MonikerStyle monikerStyle, string delimiter = DefaultDelimiter)
{
return monikerStyle switch
ValidateDelimiterArgument(delimiter);
Generate(monikerStyle, out var adjective, out var noun);
return Join(adjective, delimiter, noun);
}

/// <summary>
/// Generate a random name in the specified style.
/// </summary>
/// <param name="monikerStyle">The style of random name.</param>
/// <param name="adjective">The adjective part of the random name.</param>
/// <param name="noun">The adjective part of the random name.</param>
/// <returns>The generated random name.</returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static void Generate(MonikerStyle monikerStyle, out Chars adjective, out Chars noun)
{
switch (monikerStyle)
{
MonikerStyle.Moby => GenerateMoby(delimiter),
MonikerStyle.Moniker => GenerateMoniker(delimiter),
_ => throw new ArgumentOutOfRangeException(nameof(monikerStyle))
};
case MonikerStyle.Moby: GenerateMoby(out adjective, out noun); break;
case MonikerStyle.Moniker: GenerateMoniker(out adjective, out noun); break;
default: throw new ArgumentOutOfRangeException(nameof(monikerStyle));
}
}

/// <summary>
Expand All @@ -35,50 +52,75 @@ public static string Generate(MonikerStyle monikerStyle, string delimiter = Defa
/// <param name="delimiter">An optional delimiter to use between adjective and noun.</param>
/// <returns>The generated random name.</returns>
public static string GenerateMoniker(string delimiter = DefaultDelimiter)
=> BuildNamePair(MonikerDescriptors.Strings, MonikerAnimals.Strings, delimiter);
{
ValidateDelimiterArgument(delimiter);
GenerateMoniker(out var adjective, out var noun);
return Join(adjective, delimiter, noun);
}

/// <summary>
/// Generate a random name in the 'moniker' project style.
/// </summary>
/// <returns>The generated random name.</returns>
public static void GenerateMoniker(out Chars adjective, out Chars noun)
=> BuildNamePair(MonikerDescriptors.Strings, out adjective, MonikerAnimals.Strings, out noun);

/// <summary>
/// Generate a random name in the 'moby' project style.
/// </summary>
/// <param name="delimiter">An optional delimiter to use between adjective and noun.</param>
/// <returns>The generated random name.</returns>
public static string GenerateMoby(string delimiter = DefaultDelimiter)
=> BuildNamePair(MobyAdjectives.Strings, MobySurnames.Strings, delimiter);

private static string BuildNamePair(
Utf8Strings adjectives,
Utf8Strings nouns,
string delimiter)
{
if (string.IsNullOrEmpty(delimiter))
throw new ArgumentException("The delimiter must not be null or empty.", nameof(delimiter));
ValidateDelimiterArgument(delimiter);
GenerateMoby(out var adjective, out var noun);
return Join(adjective, delimiter, noun);
}

BuildNamePair(adjectives, out var adjective, nouns, out var noun);
private static string Join(Chars adjective, ReadOnlySpan<char> delimiter, Chars noun)
{
var length = adjective.Length + delimiter.Length + noun.Length;
var chars = length <= 64 ? stackalloc char[length] : new char[length];

var length = adjective.CharCount + delimiter.Length + noun.CharCount;
var writeCount = adjective.Write(chars);
Debug.Assert(writeCount == adjective.Length);

var chars = length <= 64 ? stackalloc char[length] : new char[length];
delimiter.CopyTo(chars[adjective.Length..]);

_ = adjective.GetChars(chars);
delimiter.CopyTo(chars[adjective.CharCount..]);
noun.GetChars(chars[(adjective.CharCount + delimiter.Length)..]);
writeCount = noun.Write(chars[(adjective.Length + delimiter.Length)..]);
Debug.Assert(writeCount == noun.Length);

return new(chars);
}

/// <summary>
/// Generate a random name in the 'moby' project style.
/// </summary>
/// <returns>The generated random name.</returns>
public static void GenerateMoby(out Chars adjective, out Chars noun)
=> BuildNamePair(MobyAdjectives.Strings, out adjective, MobySurnames.Strings, out noun);

private static void ValidateDelimiterArgument(
string delimiter,
[CallerArgumentExpression(nameof(delimiter))] string? paramName = null)
{
if (string.IsNullOrEmpty(delimiter))
throw new ArgumentException("The delimiter must not be null or empty.", paramName);
}

private static void BuildNamePair(
Utf8Strings adjectives, out Utf8String adjective,
Utf8Strings nouns, out Utf8String noun)
Utf8Strings adjectives, out Chars adjective,
Utf8Strings nouns, out Chars noun)
{
do
{
adjective = GetRandomEntry(adjectives);
noun = GetRandomEntry(nouns);
}
while (adjective == "boring"u8 && noun == "wozniak"u8); // Steve Wozniak is not boring
while (adjective.Equals("boring"u8) && noun.Equals("wozniak"u8)); // Steve Wozniak is not boring
}

private static Utf8String GetRandomEntry(Utf8Strings entries)
private static Chars GetRandomEntry(Utf8Strings entries)
{
var index = Random.Shared.Next(entries.Count);
return entries[index];
Expand Down
30 changes: 2 additions & 28 deletions src/Moniker/Utf8Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,6 @@

namespace Moniker;

internal readonly ref struct Utf8String(ReadOnlySpan<byte> bytes, int charCount)
{
public readonly ReadOnlySpan<byte> Bytes = bytes;
public readonly int CharCount = charCount;

public int GetChars(Span<char> chars) => Encoding.UTF8.GetChars(Bytes, chars);

public override string ToString() => Encoding.UTF8.GetString(Bytes);

// Equality members

public static bool operator ==(Utf8String left, ReadOnlySpan<byte> right) => left.Equals(right);
public static bool operator !=(Utf8String left, ReadOnlySpan<byte> right) => !left.Equals(right);

private bool Equals(ReadOnlySpan<byte> other) => Bytes.SequenceEqual(other);

// Unsupported equality members because this type cannot be boxed

public override int GetHashCode() => throw new NotSupportedException();
public override bool Equals(object? obj) => throw new NotSupportedException();

// Implicit conversions

public static implicit operator ReadOnlySpan<byte>(Utf8String data) => data.Bytes;
}

internal readonly ref struct Utf8Strings
{
private readonly ReadOnlySpan<byte> _data;
Expand All @@ -52,7 +26,7 @@ public Utf8Strings(int count, ReadOnlySpan<byte> data, ReadOnlySpan<int> offsets
_charCounts = charCounts;
}

public Utf8String this[int index] => new(_data[_offsets[index].._offsets[index + 1]], _charCounts[index]);
public Chars this[int index] => new(_data[_offsets[index].._offsets[index + 1]], _charCounts[index]);

public override string ToString() => $"{{ Count = {Count}, Size = {_data.Length} }}";

Expand All @@ -67,7 +41,7 @@ public ref struct Enumerator(Utf8Strings strings)
/// Behaviour is undefined if <see cref="MoveNext"/> has never been called or returned
/// <see langword="false"/>.
/// </remarks>
public Utf8String Current => _strings[_index];
public Chars Current => _strings[_index];

public bool MoveNext()
{
Expand Down
23 changes: 23 additions & 0 deletions test/Moniker.ApprovalTests/Moniker.approved.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
namespace Moniker
{
[System.Obsolete("Types with embedded references are not supported in this version of your compiler" +
".", true)]
[System.Runtime.CompilerServices.CompilerFeatureRequired("RefStructs")]
[System.Runtime.CompilerServices.IsByRefLike]
public readonly struct Chars
{
public int Length { get; }
public int Utf8Length { get; }
public bool Equals(Moniker.Chars other) { }
[System.Obsolete("This method is unsupported and will always throw an exception. Use the equality o" +
"perator instead.")]
public override bool Equals(object? obj) { }
[System.Obsolete("This method is unsupported and will always throw an exception.")]
public override int GetHashCode() { }
public override string ToString() { }
public int Write(System.Span<char> buffer) { }
public int WriteUtf8(System.Span<byte> buffer) { }
public static bool operator !=(Moniker.Chars left, Moniker.Chars right) { }
public static bool operator ==(Moniker.Chars left, Moniker.Chars right) { }
}
public enum MonikerStyle
{
Moniker = 0,
Expand All @@ -9,7 +29,10 @@ namespace Moniker
{
public const string DefaultDelimiter = "-";
public static string Generate(Moniker.MonikerStyle monikerStyle, string delimiter = "-") { }
public static void Generate(Moniker.MonikerStyle monikerStyle, out Moniker.Chars adjective, out Moniker.Chars noun) { }
public static string GenerateMoby(string delimiter = "-") { }
public static void GenerateMoby(out Moniker.Chars adjective, out Moniker.Chars noun) { }
public static string GenerateMoniker(string delimiter = "-") { }
public static void GenerateMoniker(out Moniker.Chars adjective, out Moniker.Chars noun) { }
}
}
6 changes: 6 additions & 0 deletions test/Moniker.PerformanceTests/NameGeneratorBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ public class NameGeneratorBenchmark

[Benchmark]
public string GenerateMoniker() => NameGenerator.Generate(MonikerStyle.Moniker);

[Benchmark]
public void GenerateMobyPair() => NameGenerator.Generate(MonikerStyle.Moby, out _, out _);

[Benchmark]
public void GenerateMonikerPair() => NameGenerator.Generate(MonikerStyle.Moniker, out _, out _);
}
Loading

0 comments on commit 835d13f

Please sign in to comment.