Browse Source

Merge pull request #1663 from br3aker/bitoperations-code-improvement

Numerics.cs Log2 implementation
pull/1669/head
James Jackson-South 5 years ago
committed by GitHub
parent
commit
7a550f8830
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 73
      src/ImageSharp/Common/Helpers/Numerics.cs
  2. 37
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  3. 73
      tests/ImageSharp.Tests/Common/NumericsTests.cs

73
src/ImageSharp/Common/Helpers/Numerics.cs

@ -24,24 +24,12 @@ namespace SixLabors.ImageSharp
#endif
#if !SUPPORTS_BITOPERATIONS
/// <summary>
/// Gets the counts the number of bits needed to hold an integer.
/// </summary>
private static ReadOnlySpan<byte> BitCountLut => new byte[]
private static ReadOnlySpan<byte> 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
/// <summary>
/// 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.
/// </summary>
/// <param name="number">Unsigned integer to store</param>
/// <returns>Minimum number of bits needed to store given value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int MinimumBitsToStore16(uint number)
/// <param name="value">The value.</param>
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
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
/// <param name="value">The value.</param>
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
}
}

37
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);
}
}
/// <summary>
/// Calculates how many minimum bits needed to store given value for Huffman jpeg encoding.
/// </summary>
/// <remarks>
/// This method returns 0 for input value 0. This is done specificaly for huffman encoding
/// </remarks>
/// <param name="value">The value.</param>
[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
}
}
}

73
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}");
}
}
}
}
Loading…
Cancel
Save