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}"); + } + } + } +}