diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs
index ef457f7ceb..db65b84cca 100644
--- a/src/ImageSharp/Common/Helpers/Numerics.cs
+++ b/src/ImageSharp/Common/Helpers/Numerics.cs
@@ -24,24 +24,12 @@ namespace SixLabors.ImageSharp
#endif
#if !SUPPORTS_BITOPERATIONS
- ///
- /// Gets the counts the number of bits needed to hold an integer.
- ///
- private static ReadOnlySpan BitCountLut => new byte[]
+ private static ReadOnlySpan Log2DeBruijn => new byte[32]
{
- 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5,
- 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
- 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
- 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
- 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
- 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
- 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
- 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
- 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
- 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
- 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
- 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
- 8, 8, 8,
+ 00, 09, 01, 10, 13, 21, 02, 29,
+ 11, 14, 16, 18, 22, 25, 03, 30,
+ 08, 12, 20, 28, 15, 17, 24, 07,
+ 19, 27, 23, 06, 26, 05, 04, 31
};
#endif
@@ -849,24 +837,47 @@ namespace SixLabors.ImageSharp
#endif
///
- /// Calculates how many minimum bits needed to store given value.
+ /// Calculates floored log of the specified value, base 2.
+ /// Note that by convention, input value 0 returns 0 since Log(0) is undefined.
///
- /// Unsigned integer to store
- /// Minimum number of bits needed to store given value
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int MinimumBitsToStore16(uint number)
+ /// The value.
+ public static int Log2(uint value)
{
-#if !SUPPORTS_BITOPERATIONS
- if (number < 0x100)
- {
- return BitCountLut[(int)number];
- }
-
- return 8 + BitCountLut[(int)number >> 8];
+#if SUPPORTS_BITOPERATIONS
+ return BitOperations.Log2(value);
#else
- const int bitInUnsignedInteger = sizeof(uint) * 8;
- return bitInUnsignedInteger - BitOperations.LeadingZeroCount(number);
+ return Log2SoftwareFallback(value);
#endif
}
+
+#if !SUPPORTS_BITOPERATIONS
+ ///
+ /// Calculates floored log of the specified value, base 2.
+ /// Note that by convention, input value 0 returns 0 since Log(0) is undefined.
+ /// Bit hacking with deBruijn sequence, extremely fast yet does not use any intrinsics so will work on every platform/runtime.
+ ///
+ ///
+ /// Description of this bit hacking can be found here:
+ /// https://cstheory.stackexchange.com/questions/19524/using-the-de-bruijn-sequence-to-find-the-lceil-log-2-v-rceil-of-an-integer
+ ///
+ /// The value.
+ private static int Log2SoftwareFallback(uint value)
+ {
+ // No AggressiveInlining due to large method size
+ // Has conventional contract 0->0 (Log(0) is undefined) by default, no need for if checking
+
+ // Fill trailing zeros with ones, eg 00010010 becomes 00011111
+ value |= value >> 01;
+ value |= value >> 02;
+ value |= value >> 04;
+ value |= value >> 08;
+ value |= value >> 16;
+
+ // uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check
+ return Unsafe.AddByteOffset(
+ ref MemoryMarshal.GetReference(Log2DeBruijn),
+ (IntPtr)(int)((value * 0x07C4ACDDu) >> 27)); // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here
+ }
+#endif
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index ca352397b8..860a9c3236 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -360,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
b = value - 1;
}
- int bt = Numerics.MinimumBitsToStore16((uint)a);
+ int bt = GetHuffmanEncodingLength((uint)a);
this.EmitHuff(index, (runLength << 4) | bt);
if (bt > 0)
@@ -388,5 +388,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
this.target.Write(this.emitBuffer, 0, this.emitLen);
}
}
+
+ ///
+ /// Calculates how many minimum bits needed to store given value for Huffman jpeg encoding.
+ ///
+ ///
+ /// This method returns 0 for input value 0. This is done specificaly for huffman encoding
+ ///
+ /// The value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetHuffmanEncodingLength(uint value)
+ {
+ DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max");
+#if SUPPORTS_BITOPERATIONS
+ // This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation
+ // But internal log2 is implementated like this: (31 - (int)Lzcnt.LeadingZeroCount(value))
+
+ // BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0
+ // Lzcnt would return 32 for input value of 0 - no need to check that with branching
+ // Fallback code if Lzcnt is not supported still use if-check
+ // But most modern CPUs support this instruction so this should not be a problem
+ return 32 - System.Numerics.BitOperations.LeadingZeroCount(value);
+#else
+ // Ideally:
+ // if 0 - return 0 in this case
+ // else - return log2(value) + 1
+ //
+ // Hack based on input value constaint:
+ // We know that input values are guaranteed to be maximum 16 bit large for huffman encoding
+ // We can safely shift input value for one bit -> log2(value << 1)
+ // Because of the 16 bit value constraint it won't overflow
+ // With that input value change we no longer need to add 1 before returning
+ // And this eliminates need to check if input value is zero - it is a standard convention which Log2SoftwareFallback adheres to
+ return Numerics.Log2(value << 1);
+#endif
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Common/NumericsTests.cs b/tests/ImageSharp.Tests/Common/NumericsTests.cs
new file mode 100644
index 0000000000..29eae6d488
--- /dev/null
+++ b/tests/ImageSharp.Tests/Common/NumericsTests.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SixLabors.ImageSharp.Tests.Common
+{
+ public class NumericsTests
+ {
+ private ITestOutputHelper Output { get; }
+
+ public NumericsTests(ITestOutputHelper output)
+ {
+ this.Output = output;
+ }
+
+ private static int Log2_ReferenceImplementation(uint value)
+ {
+ int n = 0;
+ while ((value >>= 1) != 0)
+ {
+ ++n;
+ }
+
+ return n;
+ }
+
+ [Fact]
+ public void Log2_ZeroConvention()
+ {
+ uint value = 0;
+ int expected = 0;
+ int actual = Numerics.Log2(value);
+
+ Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
+ }
+
+ [Fact]
+ public void Log2_PowersOfTwo()
+ {
+ for (int i = 0; i < sizeof(int) * 8; i++)
+ {
+ // from 2^0 to 2^32
+ uint value = (uint)(1 << i);
+ int expected = i;
+ int actual = Numerics.Log2(value);
+
+ Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
+ }
+ }
+
+ [Theory]
+ [InlineData(1, 100)]
+ [InlineData(2, 100)]
+ public void Log2_RandomValues(int seed, int count)
+ {
+ var rng = new Random(seed);
+ byte[] bytes = new byte[4];
+
+ for (int i = 0; i < count; i++)
+ {
+ rng.NextBytes(bytes);
+ uint value = BitConverter.ToUInt32(bytes, 0);
+ int expected = Log2_ReferenceImplementation(value);
+ int actual = Numerics.Log2(value);
+
+ Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
+ }
+ }
+ }
+}