mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2024-11-25 01:42:46 +01:00
445 lines
21 KiB
C#
445 lines
21 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System.Runtime.CompilerServices;
|
|
#if NETCOREAPP3_1_OR_GREATER
|
|
using System.Runtime.Intrinsics.X86;
|
|
#endif
|
|
|
|
namespace CommunityToolkit.HighPerformance.Helpers;
|
|
|
|
/// <summary>
|
|
/// Helpers to perform bit operations on numeric types.
|
|
/// </summary>
|
|
public static class BitHelper
|
|
{
|
|
/// <summary>
|
|
/// Checks whether or not a given bit is set.
|
|
/// </summary>
|
|
/// <param name="value">The input <see cref="uint"/> value.</param>
|
|
/// <param name="n">The position of the bit to check (in [0, 31] range).</param>
|
|
/// <returns>Whether or not the n-th bit is set.</returns>
|
|
/// <remarks>
|
|
/// This method doesn't validate <paramref name="n"/> against the valid range.
|
|
/// If the parameter is not valid, the result will just be inconsistent.
|
|
/// Additionally, no conditional branches are used to retrieve the flag.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static unsafe bool HasFlag(uint value, int n)
|
|
{
|
|
// Read the n-th bit, downcast to byte
|
|
byte flag = (byte)((value >> n) & 1);
|
|
|
|
// Reinterpret the byte to avoid the test, setnz and
|
|
// movzx instructions (asm x64). This is because the JIT
|
|
// compiler is able to optimize this reinterpret-cast as
|
|
// a single "and eax, 0x1" instruction, whereas if we had
|
|
// compared the previous computed flag against 0, the assembly
|
|
// would have had to perform the test, set the non-zero
|
|
// flag and then extend the (byte) result to eax.
|
|
return *(bool*)&flag;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether or not a given bit is set in a given bitwise lookup table.
|
|
/// This method provides a branchless, register-based (with no memory accesses) way to
|
|
/// check whether a given value is valid, according to a precomputed lookup table.
|
|
/// It is similar in behavior to <see cref="HasFlag(uint,int)"/>, with the main difference
|
|
/// being that this method will also validate the input <paramref name="x"/> parameter, and
|
|
/// will always return <see langword="false"/> if it falls outside of the expected interval.
|
|
/// Additionally, this method accepts a <paramref name="min"/> parameter, which is used to
|
|
/// decrement the input parameter <paramref name="x"/> to ensure that the range of accepted
|
|
/// values fits within the available 32 bits of the lookup table in use.
|
|
/// For more info on this optimization technique, see <see href="https://egorbo.com/llvm-range-checks.html"/>.
|
|
/// Here is how the code from the link above would be implemented using this method:
|
|
/// <code>
|
|
/// bool IsReservedCharacter(char c)
|
|
/// {
|
|
/// return BitHelper.HasLookupFlag(314575237u, c, 36);
|
|
/// }
|
|
/// </code>
|
|
/// The resulted assembly is virtually identical, with the added optimization that the one
|
|
/// produced by <see cref="HasLookupFlag(uint,int,int)"/> has no conditional branches at all.
|
|
/// </summary>
|
|
/// <param name="table">The input lookup table to use.</param>
|
|
/// <param name="x">The input value to check.</param>
|
|
/// <param name="min">The minimum accepted value for <paramref name="x"/> (defaults to 0).</param>
|
|
/// <returns>Whether or not the corresponding flag for <paramref name="x"/> is set in <paramref name="table"/>.</returns>
|
|
/// <remarks>
|
|
/// For best results, as shown in the sample code, both <paramref name="table"/> and <paramref name="min"/>
|
|
/// should be compile-time constants, so that the JIT compiler will be able to produce more efficient code.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static unsafe bool HasLookupFlag(uint table, int x, int min = 0)
|
|
{
|
|
// First, the input value is scaled down by the given minimum.
|
|
// This step will be skipped entirely if min is just the default of 0.
|
|
// The valid range is given by 32, which is the number of bits in the
|
|
// lookup table. The input value is first cast to uint so that if it was
|
|
// negative, the check will fail as well. Then, the result of this
|
|
// operation is used to compute a bitwise flag of either 0xFFFFFFFF if the
|
|
// input is accepted, or all 0 otherwise. The target bit is then extracted,
|
|
// and this value is combined with the previous mask. This is done so that
|
|
// if the shift was performed with a value that was too high, which has an
|
|
// undefined behavior and could produce a non-0 value, the mask will reset
|
|
// the final value anyway. This result is then unchecked-cast to a byte (as
|
|
// it is guaranteed to always be either 1 or 0), and then reinterpreted
|
|
// as a bool just like in the HasFlag method above, and then returned.
|
|
int i = x - min;
|
|
bool isInRange = (uint)i < 32u;
|
|
byte byteFlag = *(byte*)&isInRange;
|
|
int negativeFlag = byteFlag - 1;
|
|
int mask = ~negativeFlag;
|
|
int shift = unchecked((int)((table >> i) & 1));
|
|
int and = shift & mask;
|
|
byte result = unchecked((byte)and);
|
|
bool valid = *(bool*)&result;
|
|
|
|
return valid;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the given value has any bytes that are set to 0.
|
|
/// That is, given a <see cref="uint"/> value, which has a total of 4 bytes,
|
|
/// it checks whether any of those have all the bits set to 0.
|
|
/// </summary>
|
|
/// <param name="value">The input value to check.</param>
|
|
/// <returns>Whether <paramref name="value"/> has any bytes set to 0.</returns>
|
|
/// <remarks>
|
|
/// This method contains no branches.
|
|
/// For more background on this subject, see <see href="https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord"/>.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool HasZeroByte(uint value)
|
|
{
|
|
return ((value - 0x0101_0101u) & ~value & 0x8080_8080u) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the given value has any bytes that are set to 0.
|
|
/// This method mirrors <see cref="HasZeroByte(uint)"/>, but with <see cref="ulong"/> values.
|
|
/// </summary>
|
|
/// <param name="value">The input value to check.</param>
|
|
/// <returns>Whether <paramref name="value"/> has any bytes set to 0.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool HasZeroByte(ulong value)
|
|
{
|
|
return ((value - 0x0101_0101_0101_0101ul) & ~value & 0x8080_8080_8080_8080ul) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a byte in the input <see cref="uint"/> value matches a target value.
|
|
/// </summary>
|
|
/// <param name="value">The input value to check.</param>
|
|
/// <param name="target">The target byte to look for.</param>
|
|
/// <returns>Whether <paramref name="value"/> has any bytes set to <paramref name="target"/>.</returns>
|
|
/// <remarks>
|
|
/// This method contains no branches.
|
|
/// For more info, see <see href="https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord"/>.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool HasByteEqualTo(uint value, byte target)
|
|
{
|
|
return HasZeroByte(value ^ (0x0101_0101u * target));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a byte in the input <see cref="uint"/> value matches a target value.
|
|
/// This method mirrors <see cref="HasByteEqualTo(uint,byte)"/>, but with <see cref="ulong"/> values.
|
|
/// </summary>
|
|
/// <param name="value">The input value to check.</param>
|
|
/// <param name="target">The target byte to look for.</param>
|
|
/// <returns>Whether <paramref name="value"/> has any bytes set to <paramref name="target"/>.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool HasByteEqualTo(ulong value, byte target)
|
|
{
|
|
return HasZeroByte(value ^ (0x0101_0101_0101_0101u * target));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit to a specified value.
|
|
/// </summary>
|
|
/// <param name="value">The target <see cref="uint"/> value.</param>
|
|
/// <param name="n">The position of the bit to set or clear (in [0, 31] range).</param>
|
|
/// <param name="flag">The value to assign to the target bit.</param>
|
|
/// <remarks>
|
|
/// Just like <see cref="HasFlag(uint,int)"/>, this method doesn't validate <paramref name="n"/>
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void SetFlag(ref uint value, int n, bool flag)
|
|
{
|
|
value = SetFlag(value, n, flag);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit to a specified value.
|
|
/// </summary>
|
|
/// <param name="value">The input <see cref="uint"/> value.</param>
|
|
/// <param name="n">The position of the bit to set or clear (in [0, 31] range).</param>
|
|
/// <param name="flag">The value to assign to the target bit.</param>
|
|
/// <returns>An <see cref="uint"/> value equal to <paramref name="value"/> except for the <paramref name="n"/>-th bit.</returns>
|
|
/// <remarks>
|
|
/// Just like <see cref="HasFlag(uint,int)"/>, this method doesn't validate <paramref name="n"/>
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static unsafe uint SetFlag(uint value, int n, bool flag)
|
|
{
|
|
// Shift a bit left to the n-th position, negate the
|
|
// resulting value and perform an AND with the input value.
|
|
// This effectively clears the n-th bit of our input.
|
|
uint bit = 1u << n;
|
|
uint not = ~bit;
|
|
uint and = value & not;
|
|
|
|
// Reinterpret the flag as 1 or 0, and cast to uint,
|
|
// then we left shift the uint flag to the right position
|
|
// and perform an OR with the resulting value of the previous
|
|
// operation. This will always guaranteed to work, thanks to the
|
|
// initial code clearing that bit before setting it again.
|
|
bool copy = flag;
|
|
uint flag32 = *(byte*)©
|
|
uint shift = flag32 << n;
|
|
uint or = and | shift;
|
|
|
|
return or;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts a bit field range from a given value.
|
|
/// </summary>
|
|
/// <param name="value">The input <see cref="uint"/> value.</param>
|
|
/// <param name="start">The initial index of the range to extract (in [0, 31] range).</param>
|
|
/// <param name="length">The length of the range to extract (depends on <paramref name="start"/>).</param>
|
|
/// <returns>The value of the extracted range within <paramref name="value"/>.</returns>
|
|
/// <remarks>
|
|
/// This method doesn't validate <paramref name="start"/> and <paramref name="length"/>.
|
|
/// If either parameter is not valid, the result will just be inconsistent. The method
|
|
/// should not be used to set all the bits at once, and it is not guaranteed to work in
|
|
/// that case, which would just be equivalent to assigning the <see cref="uint"/> value.
|
|
/// Additionally, no conditional branches are used to retrieve the range.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static uint ExtractRange(uint value, byte start, byte length)
|
|
{
|
|
#if NETCOREAPP3_1_OR_GREATER
|
|
if (Bmi1.IsSupported)
|
|
{
|
|
return Bmi1.BitFieldExtract(value, start, length);
|
|
}
|
|
#endif
|
|
|
|
return (value >> start) & ((1u << length) - 1u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit field range within a target value.
|
|
/// </summary>
|
|
/// <param name="value">The target <see cref="uint"/> value.</param>
|
|
/// <param name="start">The initial index of the range to extract (in [0, 31] range).</param>
|
|
/// <param name="length">The length of the range to extract (depends on <paramref name="start"/>).</param>
|
|
/// <param name="flags">The input flags to insert in the target range.</param>
|
|
/// <remarks>
|
|
/// Just like <see cref="ExtractRange(uint,byte,byte)"/>, this method doesn't validate the parameters
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void SetRange(ref uint value, byte start, byte length, uint flags)
|
|
{
|
|
value = SetRange(value, start, length, flags);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit field range within a target value.
|
|
/// </summary>
|
|
/// <param name="value">The initial <see cref="uint"/> value.</param>
|
|
/// <param name="start">The initial index of the range to extract (in [0, 31] range).</param>
|
|
/// <param name="length">The length of the range to extract (depends on <paramref name="start"/>).</param>
|
|
/// <param name="flags">The input flags to insert in the target range.</param>
|
|
/// <returns>The updated bit field value after setting the specified range.</returns>
|
|
/// <remarks>
|
|
/// Just like <see cref="ExtractRange(uint,byte,byte)"/>, this method doesn't validate the parameters
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static uint SetRange(uint value, byte start, byte length, uint flags)
|
|
{
|
|
uint highBits = (1u << length) - 1u;
|
|
uint loadMask = highBits << start;
|
|
uint storeMask = (flags & highBits) << start;
|
|
|
|
#if NETCOREAPP3_1_OR_GREATER
|
|
if (Bmi1.IsSupported)
|
|
{
|
|
return Bmi1.AndNot(loadMask, value) | storeMask;
|
|
}
|
|
#endif
|
|
|
|
return (~loadMask & value) | storeMask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether or not a given bit is set.
|
|
/// </summary>
|
|
/// <param name="value">The input <see cref="ulong"/> value.</param>
|
|
/// <param name="n">The position of the bit to check (in [0, 63] range).</param>
|
|
/// <returns>Whether or not the n-th bit is set.</returns>
|
|
/// <remarks>
|
|
/// This method doesn't validate <paramref name="n"/> against the valid range.
|
|
/// If the parameter is not valid, the result will just be inconsistent.
|
|
/// Additionally, no conditional branches are used to retrieve the flag.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static unsafe bool HasFlag(ulong value, int n)
|
|
{
|
|
// Same logic as the uint version, see that for more info
|
|
byte flag = (byte)((value >> n) & 1);
|
|
|
|
return *(bool*)&flag;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether or not a given bit is set in a given bitwise lookup table.
|
|
/// For more info, check the XML docs of the <see cref="HasLookupFlag(uint,int,int)"/> overload.
|
|
/// </summary>
|
|
/// <param name="table">The input lookup table to use.</param>
|
|
/// <param name="x">The input value to check.</param>
|
|
/// <param name="min">The minimum accepted value for <paramref name="x"/> (defaults to 0).</param>
|
|
/// <returns>Whether or not the corresponding flag for <paramref name="x"/> is set in <paramref name="table"/>.</returns>
|
|
/// <remarks>
|
|
/// For best results, as shown in the sample code, both <paramref name="table"/> and <paramref name="min"/>
|
|
/// should be compile-time constants, so that the JIT compiler will be able to produce more efficient code.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static unsafe bool HasLookupFlag(ulong table, int x, int min = 0)
|
|
{
|
|
int i = x - min;
|
|
bool isInRange = (uint)i < 64u;
|
|
byte byteFlag = *(byte*)&isInRange;
|
|
int negativeFlag = byteFlag - 1;
|
|
int mask = ~negativeFlag;
|
|
int shift = unchecked((int)((table >> i) & 1));
|
|
int and = shift & mask;
|
|
byte result = unchecked((byte)and);
|
|
bool valid = *(bool*)&result;
|
|
|
|
return valid;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit to a specified value.
|
|
/// </summary>
|
|
/// <param name="value">The target <see cref="ulong"/> value.</param>
|
|
/// <param name="n">The position of the bit to set or clear (in [0, 63] range).</param>
|
|
/// <param name="flag">The value to assign to the target bit.</param>
|
|
/// <remarks>
|
|
/// Just like <see cref="HasFlag(ulong,int)"/>, this method doesn't validate <paramref name="n"/>
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void SetFlag(ref ulong value, int n, bool flag)
|
|
{
|
|
value = SetFlag(value, n, flag);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit to a specified value.
|
|
/// </summary>
|
|
/// <param name="value">The input <see cref="ulong"/> value.</param>
|
|
/// <param name="n">The position of the bit to set or clear (in [0, 63] range).</param>
|
|
/// <param name="flag">The value to assign to the target bit.</param>
|
|
/// <returns>An <see cref="ulong"/> value equal to <paramref name="value"/> except for the <paramref name="n"/>-th bit.</returns>
|
|
/// <remarks>
|
|
/// Just like <see cref="HasFlag(ulong,int)"/>, this method doesn't validate <paramref name="n"/>
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static unsafe ulong SetFlag(ulong value, int n, bool flag)
|
|
{
|
|
ulong bit = 1ul << n;
|
|
ulong not = ~bit;
|
|
ulong and = value & not;
|
|
bool copy = flag;
|
|
ulong flag64 = *(byte*)©
|
|
ulong shift = flag64 << n;
|
|
ulong or = and | shift;
|
|
|
|
return or;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts a bit field range from a given value.
|
|
/// </summary>
|
|
/// <param name="value">The input <see cref="ulong"/> value.</param>
|
|
/// <param name="start">The initial index of the range to extract (in [0, 63] range).</param>
|
|
/// <param name="length">The length of the range to extract (depends on <paramref name="start"/>).</param>
|
|
/// <returns>The value of the extracted range within <paramref name="value"/>.</returns>
|
|
/// <remarks>
|
|
/// This method doesn't validate <paramref name="start"/> and <paramref name="length"/>.
|
|
/// If either parameter is not valid, the result will just be inconsistent. The method
|
|
/// should not be used to set all the bits at once, and it is not guaranteed to work in
|
|
/// that case, which would just be equivalent to assigning the <see cref="ulong"/> value.
|
|
/// Additionally, no conditional branches are used to retrieve the range.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static ulong ExtractRange(ulong value, byte start, byte length)
|
|
{
|
|
#if NETCOREAPP3_1_OR_GREATER
|
|
if (Bmi1.X64.IsSupported)
|
|
{
|
|
return Bmi1.X64.BitFieldExtract(value, start, length);
|
|
}
|
|
#endif
|
|
|
|
return (value >> start) & ((1ul << length) - 1ul);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit field range within a target value.
|
|
/// </summary>
|
|
/// <param name="value">The target <see cref="ulong"/> value.</param>
|
|
/// <param name="start">The initial index of the range to extract (in [0, 63] range).</param>
|
|
/// <param name="length">The length of the range to extract (depends on <paramref name="start"/>).</param>
|
|
/// <param name="flags">The input flags to insert in the target range.</param>
|
|
/// <remarks>
|
|
/// Just like <see cref="ExtractRange(ulong,byte,byte)"/>, this method doesn't validate the parameters
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void SetRange(ref ulong value, byte start, byte length, ulong flags)
|
|
{
|
|
value = SetRange(value, start, length, flags);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bit field range within a target value.
|
|
/// </summary>
|
|
/// <param name="value">The initial <see cref="ulong"/> value.</param>
|
|
/// <param name="start">The initial index of the range to extract (in [0, 63] range).</param>
|
|
/// <param name="length">The length of the range to extract (depends on <paramref name="start"/>).</param>
|
|
/// <param name="flags">The input flags to insert in the target range.</param>
|
|
/// <returns>The updated bit field value after setting the specified range.</returns>
|
|
/// <remarks>
|
|
/// Just like <see cref="ExtractRange(ulong,byte,byte)"/>, this method doesn't validate the parameters
|
|
/// and does not contain branching instructions, so it's well suited for use in tight loops as well.
|
|
/// </remarks>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static ulong SetRange(ulong value, byte start, byte length, ulong flags)
|
|
{
|
|
ulong highBits = (1ul << length) - 1ul;
|
|
ulong loadMask = highBits << start;
|
|
ulong storeMask = (flags & highBits) << start;
|
|
|
|
#if NETCOREAPP3_1_OR_GREATER
|
|
if (Bmi1.X64.IsSupported)
|
|
{
|
|
return Bmi1.X64.AndNot(loadMask, value) | storeMask;
|
|
}
|
|
#endif
|
|
|
|
return (~loadMask & value) | storeMask;
|
|
}
|
|
}
|