Skip to content

Commit 43c28eb

Browse files
john-h-kKyaa-dostSergio0694michael-hawker
authored
Optimise Guard.IsBitwiseEqual (#3325)
* Improve Guard.IsBitwiseEqualTo performance * Fix inlining * Fix incorrect bool * Update Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs Co-authored-by: Sergio Pedri <[email protected]> * Update Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs Co-authored-by: Sergio Pedri <[email protected]> * Add suggestions Apologies for multiple commits I'm.on a phone * Add new tests * Change from SRCS.Unsafe to unsafe for consistency * Get analyzer to shut up * Fix silly mistake * Fix formatting * moar formatting * Update Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs Co-authored-by: Sergio Pedri <[email protected]> * Update Guard.Comparable.Generic.cs Co-authored-by: Kyaa-dost <[email protected]> Co-authored-by: Sergio Pedri <[email protected]> Co-authored-by: Michael Hawker MSFT (XAML Llama) <[email protected]>
1 parent 4ee4540 commit 43c28eb

File tree

2 files changed

+94
-30
lines changed

2 files changed

+94
-30
lines changed

Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Diagnostics.Contracts;
67
using System.Runtime.CompilerServices;
78

89
#nullable enable
@@ -103,17 +104,15 @@ public static void IsNotEqualTo<T>(T value, T target, string name)
103104
/// <param name="name">The name of the input parameter being tested.</param>
104105
/// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not a bitwise match for <paramref name="target"/>.</exception>
105106
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106-
public static void IsBitwiseEqualTo<T>(T value, T target, string name)
107+
public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
107108
where T : unmanaged
108109
{
109-
// Include some fast paths if the input type is of size 1, 2, 4 or 8.
110+
// Include some fast paths if the input type is of size 1, 2, 4, 8, or 16.
110111
// In those cases, just reinterpret the bytes as values of an integer type,
111112
// and compare them directly, which is much faster than having a loop over each byte.
112113
// The conditional branches below are known at compile time by the JIT compiler,
113114
// so that only the right one will actually be translated into native code.
114-
if (typeof(T) == typeof(byte) ||
115-
typeof(T) == typeof(sbyte) ||
116-
typeof(T) == typeof(bool))
115+
if (sizeof(T) == 1)
117116
{
118117
byte valueByte = Unsafe.As<T, byte>(ref value);
119118
byte targetByte = Unsafe.As<T, byte>(ref target);
@@ -125,9 +124,7 @@ public static void IsBitwiseEqualTo<T>(T value, T target, string name)
125124

126125
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
127126
}
128-
else if (typeof(T) == typeof(ushort) ||
129-
typeof(T) == typeof(short) ||
130-
typeof(T) == typeof(char))
127+
else if (sizeof(T) == 2)
131128
{
132129
ushort valueUShort = Unsafe.As<T, ushort>(ref value);
133130
ushort targetUShort = Unsafe.As<T, ushort>(ref target);
@@ -139,9 +136,7 @@ public static void IsBitwiseEqualTo<T>(T value, T target, string name)
139136

140137
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
141138
}
142-
else if (typeof(T) == typeof(uint) ||
143-
typeof(T) == typeof(int) ||
144-
typeof(T) == typeof(float))
139+
else if (sizeof(T) == 4)
145140
{
146141
uint valueUInt = Unsafe.As<T, uint>(ref value);
147142
uint targetUInt = Unsafe.As<T, uint>(ref target);
@@ -153,37 +148,66 @@ public static void IsBitwiseEqualTo<T>(T value, T target, string name)
153148

154149
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
155150
}
156-
else if (typeof(T) == typeof(ulong) ||
157-
typeof(T) == typeof(long) ||
158-
typeof(T) == typeof(double))
151+
else if (sizeof(T) == 8)
159152
{
160153
ulong valueULong = Unsafe.As<T, ulong>(ref value);
161154
ulong targetULong = Unsafe.As<T, ulong>(ref target);
162155

163-
if (valueULong == targetULong)
156+
if (Bit64Compare(ref valueULong, ref targetULong))
164157
{
165158
return;
166159
}
167160

168161
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
169162
}
170-
else
163+
else if (sizeof(T) == 16)
171164
{
172-
ref byte valueRef = ref Unsafe.As<T, byte>(ref value);
173-
ref byte targetRef = ref Unsafe.As<T, byte>(ref target);
174-
int bytesCount = Unsafe.SizeOf<T>();
165+
ulong valueULong0 = Unsafe.As<T, ulong>(ref value);
166+
ulong targetULong0 = Unsafe.As<T, ulong>(ref target);
175167

176-
for (int i = 0; i < bytesCount; i++)
168+
if (Bit64Compare(ref valueULong0, ref targetULong0))
177169
{
178-
byte valueByte = Unsafe.Add(ref valueRef, i);
179-
byte targetByte = Unsafe.Add(ref targetRef, i);
170+
ulong valueULong1 = Unsafe.Add(ref Unsafe.As<T, ulong>(ref value), 1);
171+
ulong targetULong1 = Unsafe.Add(ref Unsafe.As<T, ulong>(ref target), 1);
180172

181-
if (valueByte != targetByte)
173+
if (Bit64Compare(ref valueULong1, ref targetULong1))
182174
{
183-
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
175+
return;
184176
}
185177
}
178+
179+
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
180+
}
181+
else
182+
{
183+
Span<byte> valueBytes = new Span<byte>(Unsafe.AsPointer(ref value), sizeof(T));
184+
Span<byte> targetBytes = new Span<byte>(Unsafe.AsPointer(ref target), sizeof(T));
185+
186+
if (valueBytes.SequenceEqual(targetBytes))
187+
{
188+
return;
189+
}
190+
191+
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
192+
}
193+
}
194+
195+
// Compares 64 bits of data from two given memory locations for bitwise equality
196+
[Pure]
197+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
198+
private static unsafe bool Bit64Compare(ref ulong left, ref ulong right)
199+
{
200+
// Handles 32 bit case, because using ulong is inefficient
201+
if (sizeof(IntPtr) == 4)
202+
{
203+
ref int r0 = ref Unsafe.As<ulong, int>(ref left);
204+
ref int r1 = ref Unsafe.As<ulong, int>(ref right);
205+
206+
return r0 == r1 &&
207+
Unsafe.Add(ref r0, 1) == Unsafe.Add(ref r1, 1);
186208
}
209+
210+
return left == right;
187211
}
188212

189213
/// <summary>

UnitTests/UnitTests.Shared/Diagnostics/Test_Guard.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,65 @@ public void Test_Guard_IsNotEqualTo_Fail()
156156
public void Test_Guard_IsBitwiseEqualTo_Ok()
157157
{
158158
Guard.IsBitwiseEqualTo(byte.MaxValue, byte.MaxValue, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
159-
Guard.IsBitwiseEqualTo(DateTime.MaxValue, DateTime.MaxValue, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
160-
Guard.IsBitwiseEqualTo(double.Epsilon, double.Epsilon, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
161159
Guard.IsBitwiseEqualTo(MathF.PI, MathF.PI, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
160+
Guard.IsBitwiseEqualTo(double.Epsilon, double.Epsilon, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
161+
162+
var guid = Guid.NewGuid();
163+
Guard.IsBitwiseEqualTo(guid, guid, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
164+
165+
// tests the >16 byte case where the loop is called
166+
var biggerThanLimit = new BiggerThanLimit(0, 3, ulong.MaxValue, ulong.MinValue);
167+
Guard.IsBitwiseEqualTo(biggerThanLimit, biggerThanLimit, nameof(Test_Guard_IsBitwiseEqualTo_Ok));
168+
}
169+
170+
[TestCategory("Guard")]
171+
[TestMethod]
172+
[ExpectedException(typeof(ArgumentException))]
173+
public void Test_Guard_IsBitwiseEqualTo_Size8Fail()
174+
{
175+
Guard.IsBitwiseEqualTo(double.PositiveInfinity, double.Epsilon, nameof(Test_Guard_IsBitwiseEqualTo_Size8Fail));
176+
Guard.IsBitwiseEqualTo(DateTime.Now, DateTime.Today, nameof(Test_Guard_IsBitwiseEqualTo_Size8Fail));
162177
}
163178

164179
[TestCategory("Guard")]
165180
[TestMethod]
166181
[ExpectedException(typeof(ArgumentException))]
167-
public void Test_Guard_IsBitwiseEqualTo_SingleFail()
182+
public void Test_Guard_IsBitwiseEqualTo_Size16Fail()
168183
{
169-
Guard.IsBitwiseEqualTo(double.PositiveInfinity, double.Epsilon, nameof(Test_Guard_IsBitwiseEqualTo_SingleFail));
184+
Guard.IsBitwiseEqualTo(decimal.MaxValue, decimal.MinusOne, nameof(Test_Guard_IsBitwiseEqualTo_Size16Fail));
185+
Guard.IsBitwiseEqualTo(Guid.NewGuid(), Guid.NewGuid(), nameof(Test_Guard_IsBitwiseEqualTo_Size16Fail));
186+
}
187+
188+
// a >16 byte struct for testing IsBitwiseEqual's pathway for >16 byte types
189+
private struct BiggerThanLimit
190+
{
191+
public BiggerThanLimit(ulong a, ulong b, ulong c, ulong d)
192+
{
193+
A = a;
194+
B = b;
195+
C = c;
196+
D = d;
197+
}
198+
199+
public ulong A;
200+
201+
public ulong B;
202+
203+
public ulong C;
204+
205+
public ulong D;
170206
}
171207

172208
[TestCategory("Guard")]
173209
[TestMethod]
174210
[ExpectedException(typeof(ArgumentException))]
175-
public void Test_Guard_IsBitwiseEqualTo_LoopFail()
211+
public void Test_Guard_IsBitwiseEqualTo_SequenceEqualFail()
176212
{
177-
Guard.IsBitwiseEqualTo(DateTime.Now, DateTime.Today, nameof(Test_Guard_IsBitwiseEqualTo_LoopFail));
213+
// tests the >16 byte case where the loop is called
214+
var biggerThanLimit0 = new BiggerThanLimit(0, 3, ulong.MaxValue, ulong.MinValue);
215+
var biggerThanLimit1 = new BiggerThanLimit(long.MaxValue + 1UL, 99, ulong.MaxValue ^ 0xF7UL, ulong.MinValue ^ 5555UL);
216+
217+
Guard.IsBitwiseEqualTo(biggerThanLimit0, biggerThanLimit1, nameof(Test_Guard_IsBitwiseEqualTo_SequenceEqualFail));
178218
}
179219

180220
[TestCategory("Guard")]

0 commit comments

Comments
 (0)