Skip to content

Instantly share code, notes, and snippets.

@leandromoh
Last active December 19, 2021 05:00
Show Gist options
  • Save leandromoh/75c78e276ec2393497f858eef60bbb6c to your computer and use it in GitHub Desktop.
Save leandromoh/75c78e276ec2393497f858eef60bbb6c to your computer and use it in GitHub Desktop.
TryFormat for Enum with Span.cs
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
// ``` ini
//
// BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1440 (1909/November2019Update/19H2)
// Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET SDK=6.0.100-preview.6.21355.2
// [Host] : .NET 6.0.0 (6.0.21.35212), X64 RyuJIT
// DefaultJob : .NET 6.0.0 (6.0.21.35212), X64 RyuJIT
//
//
// ```
// | Method | N | Mean | Error | StdDev | Median | Gen 0 | Allocated |
// |------------------ |------- |-----------:|----------:|---------:|-----------:|----------:|------------:|
// | SPAN_Parse_text | 100000 | 976.3 μs | 36.55 μs | 106.6 μs | 926.1 μs | - | 128 B |
// | SPAN_Parse_number | 100000 | 1,949.5 μs | 86.76 μs | 255.8 μs | 1,913.6 μs | - | 128 B |
// | STR_Parse_text | 100000 | 2,942.4 μs | 99.17 μs | 290.8 μs | 2,806.8 μs | 570.3125 | 2,400,257 B |
// | STR_Parse_number | 100000 | 4,418.8 μs | 139.81 μs | 405.6 μs | 4,231.5 μs | 1335.9375 | 5,600,602 B |
namespace ConsoleApp1
{
public class Program
{
public static void Main(string[] args)
{
#if DEBUG
var d = new EnumParse();
d.Parse_text();
#else
var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
#endif
}
}
internal enum Color
{
Black,
White,
Yellow,
LightBlue,
}
[MemoryDiagnoser]
public class EnumParse
{
[Params(100_000)]
public int N
#if DEBUG
= 1000
#endif
;
[Benchmark]
public void Parse_text()
{
Span<char> destination = new char[50];
var charsWritten = 0;
for (var i = 0; i < N; i++)
{
var isSuccess = MyEnum<Color>.TryFormat(Color.LightBlue, destination, out charsWritten);
Debug.Assert(isSuccess);
Debug.Assert(destination.Slice(0, charsWritten).SequenceEqual(nameof(Color.LightBlue)));
}
}
[Benchmark]
public void Parse_number()
{
Span<char> destination = new char[50];
var charsWritten = 0;
var enumValue = (Color)777;
for (var i = 0; i < N; i++)
{
var isSuccess = MyEnum<Color>.TryFormat(enumValue, destination, out charsWritten);
Debug.Assert(isSuccess);
Debug.Assert(destination.Slice(0, charsWritten).SequenceEqual("777"));
}
}
[Benchmark]
public void STR_Parse_text()
{
for (var i = 0; i < N; i++)
{
var result = Color.LightBlue.ToString();
}
}
[Benchmark]
public void STR_Parse_number()
{
var enumValue = ((Color)777);
for (var i = 0; i < N; i++)
{
var result = enumValue.ToString();
}
}
}
public static class MyEnum<T>
{
private delegate (bool isSuccess, int charsWritten) FuncSpanT(T value, Span<char> destination);
private static readonly FuncSpanT format;
// Considering type T as Color, format is equivalent to follow expression:
// (Color color, Span<char> span_Char) =>
// {
// int charsWritten;
// return (color == Color.Black)
// ? (((uint)span_Char.Length) >= 5)
// ? {
// "Black".AsSpan().CopyTo(span_Char);
//
// return (True, 5);
// }
// : (False, 0)
// : (color == Color.White)
// ? (((uint)span_Char.Length) >= 5)
// ? {
// "White".AsSpan().CopyTo(span_Char);
//
// return (True, 5);
// }
// : (False, 0)
// : (color == Color.Yellow)
// ? (((uint)span_Char.Length) >= 6)
// ? {
// "Yellow".AsSpan().CopyTo(span_Char);
//
// return (True, 6);
// }
// : (False, 0)
// : (color == Color.LightBlue)
// ? (((uint)span_Char.Length) >= 9)
// ? {
// "LightBlue".AsSpan().CopyTo(span_Char);
//
// return (True, 9);
// }
// : (False, 0)
// : ((int)color).TryFormat(
// span_Char,
// out charsWritten,
// default(ReadOnlySpan<char>),
// null)
// ? (True, charsWritten)
// : (False, charsWritten);
// }
static MyEnum()
{
var f = TryFormatEnum();
format = f.Compile();
}
public static bool TryFormat(T value, Span<char> destination, out int charsWritten)
{
var result = format(value, destination);
charsWritten = result.charsWritten;
return result.isSuccess;
}
private static Expression<FuncSpanT> TryFormatEnum()
{
var enumValue = Expression.Parameter(typeof(T));
var type = enumValue.Type;
var span = Expression.Parameter(typeof(Span<char>));
Debug.Assert(type.IsEnum);
Debug.Assert(span.Type == typeof(Span<char>));
var _boolIntTupleConstructor = typeof((bool, int)).GetConstructor(new[] { typeof(bool), typeof(int) });
var under = Enum.GetUnderlyingType(type);
var charsWritten = Expression.Variable(typeof(int), "charsWritten");
var body = Enum.GetValues(type)
.Cast<object>()
.Select(color =>
{
var text = color.ToString();
var valueEquals = Expression.Equal(enumValue, Expression.Constant(color, type));
var enoughSpace = Expression.GreaterThanOrEqual(
Expression.Convert(Expression.PropertyOrField(span, "Length"), typeof(uint)),
Expression.Constant((uint)text.Length, typeof(uint)));
var strSpan = StringAsSpan(Expression.Constant(text));
var ifTrue = Expression.Block(
Expression.Call(strSpan, "CopyTo", Type.EmptyTypes, span),
Expression.Constant((true, text.Length)));
var ifFalse = Expression.Constant((false, 0));
var block = Expression.Condition(enoughSpace, ifTrue, ifFalse);
return (value: block, condition: valueEquals);
})
.Reverse()
.Aggregate(Expression.Condition(
test: Expression.Call(
Expression.Convert(enumValue, under), "TryFormat", Type.EmptyTypes, span, charsWritten,
Expression.Default(typeof(ReadOnlySpan<char>)), Expression.Constant(null, typeof(CultureInfo))),
ifTrue: CreateTuple(true, charsWritten),
ifFalse: CreateTuple(false, charsWritten)),
(acc, item) =>
Expression.Condition(
item.condition,
item.value,
acc));
var blockExpr = Expression.Block(
variables: new[] { charsWritten },
expressions: body);
var lambda = Expression.Lambda<FuncSpanT>(blockExpr, enumValue, span);
return lambda;
Expression StringAsSpan(Expression str) =>
Expression.Call(typeof(MemoryExtensions), "AsSpan", Type.EmptyTypes, str);
NewExpression CreateTuple(bool success, Expression countWritten)
{
Debug.Assert(countWritten.Type == typeof(int));
return Expression.New(_boolIntTupleConstructor, Expression.Constant(success), countWritten);
}
}
}
}
@leandromoh
Copy link
Author

coded for RecordParser project

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment