From d79cfb04c6fd19c2dd67c5742f1271ac05a70c02 Mon Sep 17 00:00:00 2001 From: Andreas Leeb Date: Fri, 13 May 2022 21:51:07 +0200 Subject: [PATCH 1/2] Optimized QuantityValue to a 128-bit union struct --- UnitsNet/QuantityValue.cs | 105 +++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 19 deletions(-) diff --git a/UnitsNet/QuantityValue.cs b/UnitsNet/QuantityValue.cs index 98cb83b493..ba3814f9e7 100644 --- a/UnitsNet/QuantityValue.cs +++ b/UnitsNet/QuantityValue.cs @@ -2,6 +2,7 @@ // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. +using System.Runtime.InteropServices; using UnitsNet.InternalHelpers; namespace UnitsNet @@ -20,31 +21,41 @@ namespace UnitsNet /// From 8 (int, long, double, decimal + each nullable) down to 2 (QuantityValue and QuantityValue?). /// This also adds more numeric types with no extra overhead, such as float, short and byte. /// - public struct QuantityValue + [StructLayout(LayoutKind.Explicit)] + public readonly struct QuantityValue { /// /// Value assigned when implicitly casting from all numeric types except , since - /// has the greatest range and is 64 bits versus 128 bits for . + /// has the greatest range. /// - private readonly double? _value; + [FieldOffset(8)] // so that it does not interfere with the Type field + private readonly double _doubleValue; /// /// Value assigned when implicitly casting from type, since it has a greater precision than /// and we want to preserve that when constructing quantities that use /// as their value type. /// - private readonly decimal? _valueDecimal; + [FieldOffset(0)] + // bytes layout: 0-1 unused, 2 exponent, 3 sign (only first bit), 4-15 number + private readonly decimal _decimalValue; - private QuantityValue(double val) + /// + /// Determines the underlying type of this . + /// + [FieldOffset(0)] // using unused byte for storing type + public readonly UnderlyingDataType Type; + + private QuantityValue(double val) : this() { - _value = Guard.EnsureValidNumber(val, nameof(val)); - _valueDecimal = null; + _doubleValue = Guard.EnsureValidNumber(val, nameof(val)); + Type = UnderlyingDataType.Double; } - private QuantityValue(decimal val) + private QuantityValue(decimal val) : this() { - _valueDecimal = val; - _value = null; + _decimalValue = val; + Type = UnderlyingDataType.Decimal; } #region To QuantityValue @@ -72,10 +83,12 @@ private QuantityValue(decimal val) /// Explicit cast from to . public static explicit operator double(QuantityValue number) - { - // double -> decimal -> zero (since we can't implement the default struct ctor) - return number._value ?? (double) number._valueDecimal.GetValueOrDefault(); - } + => number.Type switch + { + UnderlyingDataType.Decimal => (double)number._decimalValue, + UnderlyingDataType.Double => number._doubleValue, + _ => throw new System.NotImplementedException() + }; #endregion @@ -83,17 +96,71 @@ public static explicit operator double(QuantityValue number) /// Explicit cast from to . public static explicit operator decimal(QuantityValue number) - { - // decimal -> double -> zero (since we can't implement the default struct ctor) - return number._valueDecimal ?? (decimal) number._value.GetValueOrDefault(); - } + => number.Type switch + { + UnderlyingDataType.Decimal => number._decimalValue, + UnderlyingDataType.Double => (decimal)number._doubleValue, + _ => throw new System.NotImplementedException() + }; #endregion /// Returns the string representation of the numeric value. public override string ToString() + => Type switch + { + UnderlyingDataType.Decimal => _decimalValue.ToString(), + UnderlyingDataType.Double => _doubleValue.ToString(), + _ => throw new System.NotImplementedException() + }; + + /// + /// Describes the underlying type of a . + /// + public enum UnderlyingDataType : byte { - return _value.HasValue ? _value.ToString() : _valueDecimal.ToString(); + /// + /// Has to be 0 due to the bit structure of . + Decimal = 0, + /// + Double = 1 } + + #region just for debugging +#if DEBUG + [FieldOffset(0)] + private readonly byte byte0; + [FieldOffset(1)] + private readonly byte byte1; + [FieldOffset(2)] + private readonly byte byte2; + [FieldOffset(3)] + private readonly byte byte3; + [FieldOffset(4)] + private readonly byte byte4; + [FieldOffset(5)] + private readonly byte byte5; + [FieldOffset(6)] + private readonly byte byte6; + [FieldOffset(7)] + private readonly byte byte7; + [FieldOffset(8)] + private readonly byte byte8; + [FieldOffset(9)] + private readonly byte byte9; + [FieldOffset(10)] + private readonly byte byte10; + [FieldOffset(11)] + private readonly byte byte11; + [FieldOffset(12)] + private readonly byte byte12; + [FieldOffset(13)] + private readonly byte byte13; + [FieldOffset(14)] + private readonly byte byte14; + [FieldOffset(15)] + private readonly byte byte15; +#endif + #endregion } } From 14fddb7f28c23fab9f2572dd405fe5c647b0e1c9 Mon Sep 17 00:00:00 2001 From: Andreas Leeb Date: Fri, 3 Jun 2022 15:55:46 +0200 Subject: [PATCH 2/2] Improved documentation and debug view --- UnitsNet/InternalHelpers/BytesUtility.cs | 40 +++++++++++++ UnitsNet/QuantityValue.cs | 73 +++++++++--------------- 2 files changed, 67 insertions(+), 46 deletions(-) create mode 100644 UnitsNet/InternalHelpers/BytesUtility.cs diff --git a/UnitsNet/InternalHelpers/BytesUtility.cs b/UnitsNet/InternalHelpers/BytesUtility.cs new file mode 100644 index 0000000000..e75e467773 --- /dev/null +++ b/UnitsNet/InternalHelpers/BytesUtility.cs @@ -0,0 +1,40 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System; +using System.Runtime.InteropServices; + +namespace UnitsNet.InternalHelpers +{ + /// + /// Utility methods for working with the byte representation of structs. + /// + internal static class BytesUtility + { + /// + /// Converts the given to an array of its underlying bytes. + /// + /// The struct type. + /// The struct value to convert. + /// A byte array representing a copy of s bytes. + internal static byte[] GetBytes(T value) where T : struct + { + int size = Marshal.SizeOf(value); + byte[] array = new byte[size]; + + IntPtr ptr = Marshal.AllocHGlobal(size); + + try + { + Marshal.StructureToPtr(value, ptr, true); + Marshal.Copy(ptr, array, 0, size); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + + return array; + } + } +} diff --git a/UnitsNet/QuantityValue.cs b/UnitsNet/QuantityValue.cs index ba3814f9e7..d25d184e98 100644 --- a/UnitsNet/QuantityValue.cs +++ b/UnitsNet/QuantityValue.cs @@ -1,8 +1,10 @@ // Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. - +using System; +using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; using UnitsNet.InternalHelpers; namespace UnitsNet @@ -17,11 +19,15 @@ namespace UnitsNet /// /// /// - /// At the time of this writing, this reduces the number of From(value, unit) overloads to 1/4th: + /// At the time of this writing, this reduces the number of From(value, unit) overloads to 1/4th: /// From 8 (int, long, double, decimal + each nullable) down to 2 (QuantityValue and QuantityValue?). - /// This also adds more numeric types with no extra overhead, such as float, short and byte. + /// This also adds more numeric types with no extra overhead, such as float, short and byte. + /// So far, the internal representation can be either or , + /// but as this struct is realized as a union struct with overlapping fields, only the amount of memory of the largest data type is used. + /// This allows for adding support for smaller data types without increasing the overall size. /// [StructLayout(LayoutKind.Explicit)] + [DebuggerDisplay("{GetDebugRepresentation()}")] public readonly struct QuantityValue { /// @@ -37,7 +43,7 @@ public readonly struct QuantityValue /// as their value type. /// [FieldOffset(0)] - // bytes layout: 0-1 unused, 2 exponent, 3 sign (only first bit), 4-15 number + // bytes layout: 0-1 unused, 2 exponent, 3 sign (only highest bit), 4-15 number private readonly decimal _decimalValue; /// @@ -87,7 +93,7 @@ public static explicit operator double(QuantityValue number) { UnderlyingDataType.Decimal => (double)number._decimalValue, UnderlyingDataType.Double => number._doubleValue, - _ => throw new System.NotImplementedException() + _ => throw new NotImplementedException() }; #endregion @@ -100,7 +106,7 @@ public static explicit operator decimal(QuantityValue number) { UnderlyingDataType.Decimal => number._decimalValue, UnderlyingDataType.Double => (decimal)number._doubleValue, - _ => throw new System.NotImplementedException() + _ => throw new NotImplementedException() }; #endregion @@ -111,56 +117,31 @@ public override string ToString() { UnderlyingDataType.Decimal => _decimalValue.ToString(), UnderlyingDataType.Double => _doubleValue.ToString(), - _ => throw new System.NotImplementedException() + _ => throw new NotImplementedException() }; + private string GetDebugRepresentation() + { + StringBuilder builder = new($"{Type} {ToString()} Hex:"); + + byte[] bytes = BytesUtility.GetBytes(this); + for (int i = bytes.Length - 1; i >= 0; i--) + { + builder.Append($" {bytes[i]:X2}"); + } + + return builder.ToString(); + } + /// /// Describes the underlying type of a . /// public enum UnderlyingDataType : byte { - /// - /// Has to be 0 due to the bit structure of . + /// must have the value 0 due to the bit structure of . Decimal = 0, /// Double = 1 } - - #region just for debugging -#if DEBUG - [FieldOffset(0)] - private readonly byte byte0; - [FieldOffset(1)] - private readonly byte byte1; - [FieldOffset(2)] - private readonly byte byte2; - [FieldOffset(3)] - private readonly byte byte3; - [FieldOffset(4)] - private readonly byte byte4; - [FieldOffset(5)] - private readonly byte byte5; - [FieldOffset(6)] - private readonly byte byte6; - [FieldOffset(7)] - private readonly byte byte7; - [FieldOffset(8)] - private readonly byte byte8; - [FieldOffset(9)] - private readonly byte byte9; - [FieldOffset(10)] - private readonly byte byte10; - [FieldOffset(11)] - private readonly byte byte11; - [FieldOffset(12)] - private readonly byte byte12; - [FieldOffset(13)] - private readonly byte byte13; - [FieldOffset(14)] - private readonly byte byte14; - [FieldOffset(15)] - private readonly byte byte15; -#endif - #endregion } }