Skip to content

Commit 0171225

Browse files
authored
QuantityValue: 16 bytes instead of 40 bytes (#1084)
Co-authored-by: Andreas Leeb <[email protected]> Fixes #875 Regarding #875 (reply in thread) a PR draft for #875 (comment) Size comparison ``` Before: Marshal.SizeOf<QuantityValue>(): 40 B After: Marshal.SizeOf<QuantityValue>(): 16 B ``` `UnderlyingDataType` could support up to 256 different enum values, I guess we won't ever need more different underlying types that can fit into this struct 😄
1 parent 6e2f40c commit 0171225

File tree

2 files changed

+110
-22
lines changed

2 files changed

+110
-22
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
7+
namespace UnitsNet.InternalHelpers
8+
{
9+
/// <summary>
10+
/// Utility methods for working with the byte representation of structs.
11+
/// </summary>
12+
internal static class BytesUtility
13+
{
14+
/// <summary>
15+
/// Converts the given <paramref name="value"/> to an array of its underlying bytes.
16+
/// </summary>
17+
/// <typeparam name="T">The struct type.</typeparam>
18+
/// <param name="value">The struct value to convert.</param>
19+
/// <returns>A byte array representing a copy of <paramref name="value"/>s bytes.</returns>
20+
internal static byte[] GetBytes<T>(T value) where T : struct
21+
{
22+
int size = Marshal.SizeOf(value);
23+
byte[] array = new byte[size];
24+
25+
IntPtr ptr = Marshal.AllocHGlobal(size);
26+
27+
try
28+
{
29+
Marshal.StructureToPtr(value, ptr, true);
30+
Marshal.Copy(ptr, array, 0, size);
31+
}
32+
finally
33+
{
34+
Marshal.FreeHGlobal(ptr);
35+
}
36+
37+
return array;
38+
}
39+
}
40+
}

UnitsNet/QuantityValue.cs

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Licensed under MIT No Attribution, see LICENSE file at the root.
22
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
33

4-
4+
using System;
5+
using System.Diagnostics;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
58
using UnitsNet.InternalHelpers;
69

710
namespace UnitsNet
@@ -16,35 +19,49 @@ namespace UnitsNet
1619
/// </list>
1720
/// </summary>
1821
/// <remarks>
19-
/// At the time of this writing, this reduces the number of From(value, unit) overloads to 1/4th:
22+
/// <para>At the time of this writing, this reduces the number of From(value, unit) overloads to 1/4th:
2023
/// From 8 (int, long, double, decimal + each nullable) down to 2 (QuantityValue and QuantityValue?).
21-
/// This also adds more numeric types with no extra overhead, such as float, short and byte.
24+
/// This also adds more numeric types with no extra overhead, such as float, short and byte.</para>
25+
/// <para>So far, the internal representation can be either <see cref="double"/> or <see cref="decimal"/>,
26+
/// 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.
27+
/// This allows for adding support for smaller data types without increasing the overall size.</para>
2228
/// </remarks>
23-
public struct QuantityValue
29+
[StructLayout(LayoutKind.Explicit)]
30+
[DebuggerDisplay("{GetDebugRepresentation()}")]
31+
public readonly struct QuantityValue
2432
{
2533
/// <summary>
2634
/// Value assigned when implicitly casting from all numeric types except <see cref="decimal" />, since
27-
/// <see cref="double" /> has the greatest range and is 64 bits versus 128 bits for <see cref="decimal"/>.
35+
/// <see cref="double" /> has the greatest range.
2836
/// </summary>
29-
private readonly double? _value;
37+
[FieldOffset(8)] // so that it does not interfere with the Type field
38+
private readonly double _doubleValue;
3039

3140
/// <summary>
3241
/// Value assigned when implicitly casting from <see cref="decimal" /> type, since it has a greater precision than
3342
/// <see cref="double"/> and we want to preserve that when constructing quantities that use <see cref="decimal"/>
3443
/// as their value type.
3544
/// </summary>
36-
private readonly decimal? _valueDecimal;
45+
[FieldOffset(0)]
46+
// bytes layout: 0-1 unused, 2 exponent, 3 sign (only highest bit), 4-15 number
47+
private readonly decimal _decimalValue;
48+
49+
/// <summary>
50+
/// Determines the underlying type of this <see cref="QuantityValue"/>.
51+
/// </summary>
52+
[FieldOffset(0)] // using unused byte for storing type
53+
public readonly UnderlyingDataType Type;
3754

38-
private QuantityValue(double val)
55+
private QuantityValue(double val) : this()
3956
{
40-
_value = Guard.EnsureValidNumber(val, nameof(val));
41-
_valueDecimal = null;
57+
_doubleValue = Guard.EnsureValidNumber(val, nameof(val));
58+
Type = UnderlyingDataType.Double;
4259
}
4360

44-
private QuantityValue(decimal val)
61+
private QuantityValue(decimal val) : this()
4562
{
46-
_valueDecimal = val;
47-
_value = null;
63+
_decimalValue = val;
64+
Type = UnderlyingDataType.Decimal;
4865
}
4966

5067
#region To QuantityValue
@@ -72,28 +89,59 @@ private QuantityValue(decimal val)
7289

7390
/// <summary>Explicit cast from <see cref="QuantityValue"/> to <see cref="double"/>.</summary>
7491
public static explicit operator double(QuantityValue number)
75-
{
76-
// double -> decimal -> zero (since we can't implement the default struct ctor)
77-
return number._value ?? (double) number._valueDecimal.GetValueOrDefault();
78-
}
92+
=> number.Type switch
93+
{
94+
UnderlyingDataType.Decimal => (double)number._decimalValue,
95+
UnderlyingDataType.Double => number._doubleValue,
96+
_ => throw new NotImplementedException()
97+
};
7998

8099
#endregion
81100

82101
#region To decimal
83102

84103
/// <summary>Explicit cast from <see cref="QuantityValue"/> to <see cref="decimal"/>.</summary>
85104
public static explicit operator decimal(QuantityValue number)
86-
{
87-
// decimal -> double -> zero (since we can't implement the default struct ctor)
88-
return number._valueDecimal ?? (decimal) number._value.GetValueOrDefault();
89-
}
105+
=> number.Type switch
106+
{
107+
UnderlyingDataType.Decimal => number._decimalValue,
108+
UnderlyingDataType.Double => (decimal)number._doubleValue,
109+
_ => throw new NotImplementedException()
110+
};
90111

91112
#endregion
92113

93114
/// <summary>Returns the string representation of the numeric value.</summary>
94115
public override string ToString()
116+
=> Type switch
117+
{
118+
UnderlyingDataType.Decimal => _decimalValue.ToString(),
119+
UnderlyingDataType.Double => _doubleValue.ToString(),
120+
_ => throw new NotImplementedException()
121+
};
122+
123+
private string GetDebugRepresentation()
124+
{
125+
StringBuilder builder = new($"{Type} {ToString()} Hex:");
126+
127+
byte[] bytes = BytesUtility.GetBytes(this);
128+
for (int i = bytes.Length - 1; i >= 0; i--)
129+
{
130+
builder.Append($" {bytes[i]:X2}");
131+
}
132+
133+
return builder.ToString();
134+
}
135+
136+
/// <summary>
137+
/// Describes the underlying type of a <see cref="QuantityValue"/>.
138+
/// </summary>
139+
public enum UnderlyingDataType : byte
95140
{
96-
return _value.HasValue ? _value.ToString() : _valueDecimal.ToString();
141+
/// <summary><see cref="Decimal"/> must have the value 0 due to the bit structure of <see cref="decimal"/>.</summary>
142+
Decimal = 0,
143+
/// <inheritdoc cref="double"/>
144+
Double = 1
97145
}
98146
}
99147
}

0 commit comments

Comments
 (0)