Created
October 10, 2025 16:18
-
-
Save teoadal/a4e2e1b7f2f2954f1d08402f42102a7c to your computer and use it in GitHub Desktop.
Переменные без боксинга
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Dynamic; | |
| using System.Globalization; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| #pragma warning disable CS8618, CS9264 | |
| namespace ConsoleApp1; | |
| [SimpleJob(RuntimeMoniker.Net90)] | |
| [MeanColumn, MemoryDiagnoser] | |
| public class GenericValueBench | |
| { | |
| private const bool BoolValue = true; | |
| private static readonly DateTime DateTimeValue = DateTime.Now; | |
| private const double DoubleValue = 3.3d; | |
| private const decimal DecimalValue = 4.4M; | |
| private const float FloatValue = 2.2f; | |
| private const int IntValue = 1; | |
| private const long LongValue = 5L; | |
| private const string StringValue = "it's string"; | |
| private static readonly object EmptyObject = new(); | |
| private static readonly char[] Buffer = new char[1024]; | |
| private static readonly CultureInfo Culture = CultureInfo.InvariantCulture; | |
| [Benchmark(Baseline = true)] | |
| public int Simple() | |
| { | |
| var values = GC.AllocateUninitializedArray<GenericValue>(9); | |
| values[0] = new GenericValue(IntValue); | |
| values[1] = new GenericValue(FloatValue); | |
| values[2] = new GenericValue(DoubleValue); | |
| values[3] = new GenericValue(DecimalValue); | |
| values[4] = new GenericValue(LongValue); | |
| values[5] = new GenericValue(StringValue); | |
| values[6] = new GenericValue(DateTimeValue); | |
| values[7] = new GenericValue(BoolValue); | |
| values[8] = new GenericValue(string.Empty); | |
| Span<char> buffer = Buffer; | |
| var result = 0; | |
| foreach (var value in values) | |
| { | |
| value.TryFormat(ref buffer, out var charsWritten, Culture); | |
| result += charsWritten; | |
| } | |
| return result; | |
| } | |
| [Benchmark] | |
| public int Better() | |
| { | |
| var values = GC.AllocateUninitializedArray<GenericValueBetter>(9); | |
| values[0] = new GenericValueBetter(IntValue); | |
| values[1] = new GenericValueBetter(FloatValue); | |
| values[2] = new GenericValueBetter(DoubleValue); | |
| values[3] = new GenericValueBetter(DecimalValue); | |
| values[4] = new GenericValueBetter(LongValue); | |
| values[5] = new GenericValueBetter(StringValue); | |
| values[6] = new GenericValueBetter(DateTimeValue); | |
| values[7] = new GenericValueBetter(BoolValue); | |
| values[8] = new GenericValueBetter(string.Empty); | |
| Span<char> buffer = Buffer; | |
| var result = 0; | |
| foreach (var value in values) | |
| { | |
| value.TryFormat(ref buffer, out var charsWritten, Culture); | |
| result += charsWritten; | |
| } | |
| return result; | |
| } | |
| [Benchmark] | |
| public int Optimal() | |
| { | |
| var values = GC.AllocateUninitializedArray<GenericValueOptimal>(9); | |
| values[0] = new GenericValueOptimal(IntValue); | |
| values[1] = new GenericValueOptimal(FloatValue); | |
| values[2] = new GenericValueOptimal(DoubleValue); | |
| values[3] = new GenericValueOptimal(DecimalValue); | |
| values[4] = new GenericValueOptimal(LongValue); | |
| values[5] = new GenericValueOptimal(StringValue); | |
| values[6] = new GenericValueOptimal(DateTimeValue); | |
| values[7] = new GenericValueOptimal(BoolValue); | |
| values[8] = new GenericValueOptimal(string.Empty); | |
| Span<char> buffer = Buffer; | |
| var result = 0; | |
| foreach (var value in values) | |
| { | |
| value.TryFormat(ref buffer, out var charsWritten, Culture); | |
| result += charsWritten; | |
| } | |
| return result; | |
| } | |
| [Benchmark] | |
| public int Best() | |
| { | |
| var values = GC.AllocateUninitializedArray<object>(9); | |
| values[0] = IntValue; | |
| values[1] = FloatValue; | |
| values[2] = DoubleValue; | |
| values[3] = DecimalValue; | |
| values[4] = LongValue; | |
| values[5] = StringValue; | |
| values[6] = DateTimeValue; | |
| values[7] = BoolValue; | |
| values[8] = EmptyObject; | |
| Span<char> buffer = Buffer; | |
| var result = 0; | |
| foreach (var value in values) | |
| { | |
| if (value is ISpanFormattable spanFormattable) | |
| { | |
| spanFormattable.TryFormat( | |
| buffer, | |
| out var charsWritten, | |
| ReadOnlySpan<char>.Empty, | |
| Culture); | |
| result += charsWritten; | |
| } | |
| else | |
| { | |
| result += value.ToString()?.Length ?? 0; | |
| } | |
| } | |
| return result; | |
| } | |
| [Benchmark] | |
| public int Poor_Dynamic() | |
| { | |
| dynamic values = new ExpandoObject(); | |
| values.Int = IntValue; | |
| values.Float = FloatValue; | |
| values.Double = DoubleValue; | |
| values.Decimal = DecimalValue; | |
| values.Long = LongValue; | |
| values.String = StringValue; | |
| values.DateTime = DateTimeValue; | |
| values.Bool = BoolValue; | |
| values.Null = string.Empty; | |
| Span<char> buffer = Buffer; | |
| var result = 0; | |
| foreach (var value in values) | |
| { | |
| if (value is ISpanFormattable spanFormattable) | |
| { | |
| spanFormattable.TryFormat( | |
| buffer, | |
| out var charsWritten, | |
| ReadOnlySpan<char>.Empty, | |
| Culture); | |
| result += charsWritten; | |
| } | |
| else | |
| { | |
| result += value.ToString().Length; | |
| } | |
| } | |
| return result; | |
| } | |
| } | |
| public readonly struct GenericValue | |
| { | |
| private readonly float _floatValue; | |
| private readonly double _doubleValue; | |
| private readonly DateTime _dateTimeValue; | |
| private readonly int _intValue; | |
| private readonly decimal _decimalValue; | |
| private readonly long _longValue; | |
| private readonly string _stringValue; | |
| private readonly GenericValueType _type; | |
| private readonly bool _boolValue; | |
| #region Constructors | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValue() => Unsafe.SkipInit(out this); | |
| public GenericValue(int? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _intValue = value.Value; | |
| _type = GenericValueType.Int; | |
| } | |
| } | |
| public GenericValue(float? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _floatValue = value.Value; | |
| _type = GenericValueType.Float; | |
| } | |
| } | |
| public GenericValue(double? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _doubleValue = value.Value; | |
| _type = GenericValueType.Double; | |
| } | |
| } | |
| public GenericValue(decimal? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _decimalValue = value.Value; | |
| _type = GenericValueType.Decimal; | |
| } | |
| } | |
| public GenericValue(string? value) : this() | |
| { | |
| if (string.IsNullOrEmpty(value)) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _stringValue = value; | |
| _type = GenericValueType.String; | |
| } | |
| } | |
| public GenericValue(DateTime? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _dateTimeValue = value.Value; | |
| _type = GenericValueType.DateTime; | |
| } | |
| } | |
| public GenericValue(long? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _longValue = value.Value; | |
| _type = GenericValueType.Long; | |
| } | |
| } | |
| public GenericValue(bool? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _boolValue = value.Value; | |
| _type = GenericValueType.Bool; | |
| } | |
| } | |
| #endregion | |
| public override string ToString() => ToString(CultureInfo.InvariantCulture); | |
| public string ToString(CultureInfo cultureInfo) | |
| { | |
| if (_type == GenericValueType.String) return _stringValue; | |
| Span<char> buffer = stackalloc char[256]; | |
| return TryFormat(ref buffer, out var written, cultureInfo) | |
| ? new string(buffer[..written]) | |
| : "EMPTY"; | |
| } | |
| public bool TryFormat(ref Span<char> buffer, out int charsWritten, CultureInfo cultureInfo) | |
| { | |
| switch (_type) | |
| { | |
| case GenericValueType.Null: | |
| const string nullValue = "EMPTY"; | |
| nullValue.CopyTo(buffer); | |
| charsWritten = nullValue.Length; | |
| return true; | |
| case GenericValueType.Bool: | |
| return _boolValue.TryFormat(buffer, out charsWritten); | |
| case GenericValueType.DateTime: | |
| return _dateTimeValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Double: | |
| return _doubleValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Decimal: | |
| return _decimalValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Float: | |
| return _floatValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Int: | |
| return _intValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Long: | |
| return _longValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.String: | |
| _stringValue.CopyTo(buffer); | |
| charsWritten = _stringValue.Length; | |
| return true; | |
| default: | |
| const string emptyValue = "EMPTY"; | |
| emptyValue.CopyTo(buffer); | |
| charsWritten = emptyValue.Length; | |
| return true; | |
| } | |
| } | |
| } | |
| [StructLayout(LayoutKind.Auto)] | |
| public readonly struct GenericValueBetter | |
| { | |
| private readonly double _doubleValue; // + float + DateTime | |
| private readonly decimal _decimalValue; // + int + long + bool + DateOnly | |
| private readonly string _stringValue; | |
| private readonly GenericValueType _type; | |
| #region Constructors | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueBetter() => Unsafe.SkipInit(out this); | |
| public GenericValueBetter(int? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _decimalValue = value.Value; | |
| _type = GenericValueType.Int; | |
| } | |
| } | |
| public GenericValueBetter(float? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _doubleValue = value.Value; | |
| _type = GenericValueType.Float; | |
| } | |
| } | |
| public GenericValueBetter(double? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _doubleValue = value.Value; | |
| _type = GenericValueType.Double; | |
| } | |
| } | |
| public GenericValueBetter(decimal? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _decimalValue = value.Value; | |
| _type = GenericValueType.Decimal; | |
| } | |
| } | |
| public GenericValueBetter(string? value) : this() | |
| { | |
| if (string.IsNullOrEmpty(value)) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _stringValue = value; | |
| _type = GenericValueType.String; | |
| } | |
| } | |
| public GenericValueBetter(DateTime? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _doubleValue = value.Value.ToOADate(); | |
| _type = GenericValueType.DateTime; | |
| } | |
| } | |
| public GenericValueBetter(bool? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _decimalValue = value.Value ? 1 : 0; | |
| _type = GenericValueType.Bool; | |
| } | |
| } | |
| public GenericValueBetter(long? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _decimalValue = value.Value; | |
| _type = GenericValueType.Long; | |
| } | |
| } | |
| #endregion | |
| public override string ToString() => ToString(CultureInfo.InvariantCulture); | |
| public string ToString(CultureInfo cultureInfo) | |
| { | |
| if (_type == GenericValueType.String) return _stringValue; | |
| Span<char> buffer = stackalloc char[256]; | |
| return TryFormat(ref buffer, out var written, cultureInfo) | |
| ? new string(buffer[..written]) | |
| : "EMPTY"; | |
| } | |
| public bool TryFormat(ref Span<char> buffer, out int charsWritten, CultureInfo cultureInfo) | |
| { | |
| switch (_type) | |
| { | |
| case GenericValueType.Null: | |
| const string nullValue = "EMPTY"; | |
| nullValue.CopyTo(buffer); | |
| charsWritten = nullValue.Length; | |
| return true; | |
| case GenericValueType.Bool: | |
| return (_decimalValue == 1).TryFormat(buffer, out charsWritten); | |
| case GenericValueType.DateTime: | |
| return DateTime.FromOADate(_doubleValue).TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Double: | |
| case GenericValueType.Float: | |
| return _doubleValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Decimal: | |
| case GenericValueType.Int: | |
| case GenericValueType.Long: | |
| return _decimalValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.String: | |
| _stringValue.CopyTo(buffer); | |
| charsWritten = _stringValue.Length; | |
| return true; | |
| default: | |
| const string emptyValue = "EMPTY"; | |
| emptyValue.CopyTo(buffer); | |
| charsWritten = emptyValue.Length; | |
| return true; | |
| } | |
| } | |
| } | |
| [StructLayout(LayoutKind.Auto)] | |
| public readonly struct GenericValueOptimal | |
| { | |
| private readonly string _stringValue; | |
| private readonly GenericValueNumber _numberValue; | |
| private readonly GenericValueType _type; | |
| #region Constructors | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueOptimal() => Unsafe.SkipInit(out this); | |
| public GenericValueOptimal(int? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value); | |
| _type = GenericValueType.Int; | |
| } | |
| } | |
| public GenericValueOptimal(float? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value); | |
| _type = GenericValueType.Float; | |
| } | |
| } | |
| public GenericValueOptimal(double? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value); | |
| _type = GenericValueType.Double; | |
| } | |
| } | |
| public GenericValueOptimal(decimal? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value); | |
| _type = GenericValueType.Decimal; | |
| } | |
| } | |
| public GenericValueOptimal(string? value) : this() | |
| { | |
| if (string.IsNullOrEmpty(value)) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _stringValue = value; | |
| _type = GenericValueType.String; | |
| } | |
| } | |
| public GenericValueOptimal(DateTime? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value.ToOADate()); | |
| _type = GenericValueType.DateTime; | |
| } | |
| } | |
| public GenericValueOptimal(bool? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value ? 1 : 0); | |
| _type = GenericValueType.Bool; | |
| } | |
| } | |
| public GenericValueOptimal(long? value) : this() | |
| { | |
| if (value == null) | |
| { | |
| _type = GenericValueType.Null; | |
| } | |
| else | |
| { | |
| _numberValue = new GenericValueNumber(value.Value); | |
| _type = GenericValueType.Long; | |
| } | |
| } | |
| #endregion | |
| public override string ToString() => ToString(CultureInfo.InvariantCulture); | |
| public string ToString(CultureInfo cultureInfo) | |
| { | |
| if (_type == GenericValueType.String) return _stringValue; | |
| Span<char> buffer = stackalloc char[256]; | |
| return TryFormat(ref buffer, out var written, cultureInfo) | |
| ? new string(buffer[..written]) | |
| : "EMPTY"; | |
| } | |
| public bool TryFormat(ref Span<char> buffer, out int charsWritten, CultureInfo cultureInfo) | |
| { | |
| switch (_type) | |
| { | |
| case GenericValueType.Null: | |
| const string nullValue = "EMPTY"; | |
| nullValue.CopyTo(buffer); | |
| charsWritten = nullValue.Length; | |
| return true; | |
| case GenericValueType.Bool: | |
| return (_numberValue.IntValue == 1).TryFormat(buffer, out charsWritten); | |
| case GenericValueType.DateTime: | |
| return DateTime | |
| .FromOADate(_numberValue.DoubleValue) | |
| .TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Double: | |
| return _numberValue.DoubleValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Float: | |
| return _numberValue.FloatValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Decimal: | |
| return _numberValue.DecimalValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Int: | |
| return _numberValue.IntValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.Long: | |
| return _numberValue.LongValue.TryFormat(buffer, out charsWritten, provider: cultureInfo); | |
| case GenericValueType.String: | |
| _stringValue.CopyTo(buffer); | |
| charsWritten = _stringValue.Length; | |
| return true; | |
| default: | |
| const string emptyValue = "EMPTY"; | |
| emptyValue.CopyTo(buffer); | |
| charsWritten = emptyValue.Length; | |
| return true; | |
| } | |
| } | |
| } | |
| public enum GenericValueType : byte | |
| { | |
| Null = 0, | |
| Bool, | |
| DateTime, | |
| Decimal, | |
| Double, | |
| Float, | |
| Int, | |
| Long, | |
| String | |
| } | |
| [StructLayout(LayoutKind.Explicit)] | |
| public readonly struct GenericValueNumber | |
| { | |
| [FieldOffset(0)] public readonly int IntValue; | |
| [FieldOffset(0)] public readonly float FloatValue; | |
| [FieldOffset(0)] public readonly double DoubleValue; | |
| [FieldOffset(0)] public readonly decimal DecimalValue; | |
| [FieldOffset(0)] public readonly long LongValue; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueNumber() => Unsafe.SkipInit(out this); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueNumber(int value) : this() => IntValue = value; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueNumber(float value) : this() => FloatValue = value; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueNumber(double value) : this() => DoubleValue = value; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueNumber(decimal value) : this() => DecimalValue = value; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public GenericValueNumber(long value) : this() => LongValue = value; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment