From ce6fdf9ba894cf63d4fce864fafe2fdc11cf7c76 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 14 Sep 2017 04:11:04 +0200 Subject: [PATCH] SimdUtils.BulkConvertNormalizedFloatToByte() --- src/ImageSharp/Common/Extensions/SimdUtils.cs | 128 ++++++++++++++++++ src/ImageSharp/Memory/BufferArea{T}.cs | 10 +- .../ImageSharp.Tests/Common/SimdUtilsTests.cs | 91 ++++++++++++- .../Formats/Jpg/JpegColorConverterTests.cs | 3 +- .../Formats/Jpg/JpegProfilingBenchmarks.cs | 25 +++- .../TestUtilities/TestDataGenerator.cs | 32 +++++ 6 files changed, 275 insertions(+), 14 deletions(-) create mode 100644 tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs diff --git a/src/ImageSharp/Common/Extensions/SimdUtils.cs b/src/ImageSharp/Common/Extensions/SimdUtils.cs index be9406367..c9acbc9fc 100644 --- a/src/ImageSharp/Common/Extensions/SimdUtils.cs +++ b/src/ImageSharp/Common/Extensions/SimdUtils.cs @@ -7,11 +7,27 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp { + using System.Diagnostics; + /// /// Various extension and utility methods for and utilizing SIMD capabilities /// internal static class SimdUtils { + /// + /// Indicates AVX2 architecture where both float and integer registers are of size 256 byte. + /// + public static readonly bool IsAvx2 = Vector.Count == 8 && Vector.Count == 8; + + [Conditional("DEBUG")] + internal static void GuardAvx2(string operation) + { + if (!IsAvx2) + { + throw new NotSupportedException($"{operation} is supported only on AVX2 CPU!"); + } + } + /// /// Transform all scalars in 'v' in a way that converting them to would have rounding semantics. /// @@ -41,5 +57,117 @@ namespace SixLabors.ImageSharp Vector sub0 = Vector.Subtract(add0, or0); return sub0; } + + /// + /// Convert 'source.Length' values normalized into [0..1] from 'source' into 'dest' buffer of values. + /// The values gonna be scaled up into [0-255] and rounded. + /// Based on: + /// + /// http://lolengine.net/blog/2011/3/20/understanding-fast-float-integer-conversions + /// + /// + internal static void BulkConvertNormalizedFloatToByte(ReadOnlySpan source, Span dest) + { + GuardAvx2(nameof(BulkConvertNormalizedFloatToByte)); + + DebugGuard.IsTrue((source.Length % Vector.Count) == 0, nameof(source), "source.Length should be divisable by Vector.Count!"); + + if (source.Length == 0) + { + return; + } + + ref Vector srcBase = ref Unsafe.As>(ref source.DangerousGetPinnableReference()); + ref Octet.OfByte destBase = ref Unsafe.As(ref dest.DangerousGetPinnableReference()); + + Vector magick = new Vector(32768.0f); + Vector scale = new Vector(255f) / new Vector(256f); + + int n = source.Length; + + for (int i = 0; i < n; i++) + { + // union { float f; uint32_t i; } u; + // u.f = 32768.0f + x * (255.0f / 256.0f); + // return (uint8_t)u.i; + Vector x = Unsafe.Add(ref srcBase, i); + x = (x * scale) + magick; + + Vector u = Vector.AsVectorUInt32(x); + + Octet.OfUInt32 ii = Unsafe.As, Octet.OfUInt32>(ref u); + + ref Octet.OfByte d = ref Unsafe.Add(ref destBase, i); + d.LoadFrom(ref ii); + } + } + + /// + /// Same as but clamps overflown values before conversion. + /// + internal static void BulkConvertNormalizedFloatToByteClampOverflows(ReadOnlySpan source, Span dest) + { + GuardAvx2(nameof(BulkConvertNormalizedFloatToByte)); + + DebugGuard.IsTrue((source.Length % Vector.Count) == 0, nameof(source), "source.Length should be divisable by Vector.Count!"); + + if (source.Length == 0) + { + return; + } + + ref Vector srcBase = ref Unsafe.As>(ref source.DangerousGetPinnableReference()); + ref Octet.OfByte destBase = ref Unsafe.As(ref dest.DangerousGetPinnableReference()); + + Vector magick = new Vector(32768.0f); + Vector scale = new Vector(255f) / new Vector(256f); + + int n = source.Length; + + for (int i = 0; i < n; i++) + { + // union { float f; uint32_t i; } u; + // u.f = 32768.0f + x * (255.0f / 256.0f); + // return (uint8_t)u.i; + Vector x = Unsafe.Add(ref srcBase, i); + x = Vector.Max(x, Vector.Zero); + x = Vector.Min(x, Vector.One); + + x = (x * scale) + magick; + + Vector u = Vector.AsVectorUInt32(x); + + Octet.OfUInt32 ii = Unsafe.As, Octet.OfUInt32>(ref u); + + ref Octet.OfByte d = ref Unsafe.Add(ref destBase, i); + d.LoadFrom(ref ii); + } + } + +#pragma warning disable SA1132 // Do not combine fields + private static class Octet + { + public struct OfUInt32 + { + public uint V0, V1, V2, V3, V4, V5, V6, V7; + } + + public struct OfByte + { + public byte V0, V1, V2, V3, V4, V5, V6, V7; + + public void LoadFrom(ref OfUInt32 i) + { + this.V0 = (byte)i.V0; + this.V1 = (byte)i.V1; + this.V2 = (byte)i.V2; + this.V3 = (byte)i.V3; + this.V4 = (byte)i.V4; + this.V5 = (byte)i.V5; + this.V6 = (byte)i.V6; + this.V7 = (byte)i.V7; + } + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Memory/BufferArea{T}.cs b/src/ImageSharp/Memory/BufferArea{T}.cs index 8ead22680..b5ed3566f 100644 --- a/src/ImageSharp/Memory/BufferArea{T}.cs +++ b/src/ImageSharp/Memory/BufferArea{T}.cs @@ -17,17 +17,19 @@ namespace SixLabors.ImageSharp.Memory /// public readonly Rectangle Rectangle; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public BufferArea(IBuffer2D destinationBuffer, Rectangle rectangle) { - Guard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); - Guard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); - Guard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle)); - Guard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle)); + DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); + DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle)); this.DestinationBuffer = destinationBuffer; this.Rectangle = rectangle; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public BufferArea(IBuffer2D destinationBuffer) : this(destinationBuffer, destinationBuffer.FullRectangle()) { diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index 6f7dc09d1..cb2591999 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs @@ -5,7 +5,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Common { + using System.Linq; + using System.Runtime.CompilerServices; + using Xunit.Abstractions; + using Xunit.Sdk; public class SimdUtilsTests { @@ -64,14 +68,13 @@ namespace SixLabors.ImageSharp.Tests.Common return new Vector(data); } - private static Vector CreateRandomTestVector(int seed, float scale) + private static Vector CreateRandomTestVector(int seed, float min, float max) { float[] data = new float[Vector.Count]; Random rnd = new Random(); for (int i = 0; i < Vector.Count; i++) { - float v = (float)rnd.NextDouble() - 0.5f; - v *= 2 * scale; + float v = (float)rnd.NextDouble() * (max-min) + min; data[i] = v; } return new Vector(data); @@ -97,7 +100,7 @@ namespace SixLabors.ImageSharp.Tests.Common [InlineData(42, 1000f)] public void FastRound_RandomValues(int seed, float scale) { - Vector v = CreateRandomTestVector(seed, scale); + Vector v = CreateRandomTestVector(seed, -scale*0.5f, scale*0.5f); Vector r = v.FastRound(); this.Output.WriteLine(v.ToString()); @@ -106,6 +109,86 @@ namespace SixLabors.ImageSharp.Tests.Common AssertEvenRoundIsCorrect(r, v); } + [Theory] + [InlineData(1, 0)] + [InlineData(1, 8)] + [InlineData(2, 16)] + [InlineData(3, 128)] + public void BulkConvertNormalizedFloatToByte_WithRoundedData(int seed, int count) + { + float[] orig = new Random(seed).GenerateRandomRoundedFloatArray(count, 0, 256); + float[] normalized = orig.Select(f => f / 255f).ToArray(); + + byte[] dest = new byte[count]; + + SimdUtils.BulkConvertNormalizedFloatToByte(normalized, dest); + + byte[] expected = orig.Select(f => (byte)(f)).ToArray(); + + Assert.Equal(expected, dest); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(1, 8)] + [InlineData(2, 16)] + [InlineData(3, 128)] + public void BulkConvertNormalizedFloatToByte_WithNonRoundedData(int seed, int count) + { + float[] source = new Random(seed).GenerateRandomFloatArray(count, 0, 1f); + + byte[] dest = new byte[count]; + + SimdUtils.BulkConvertNormalizedFloatToByte(source, dest); + + byte[] expected = source.Select(f => (byte)Math.Round(f*255f)).ToArray(); + + Assert.Equal(expected, dest); + } + + private static float Clamp255(float x) => MathF.Min(255f, MathF.Max(0f, x)); + + [Theory] + [InlineData(1, 0)] + [InlineData(1, 8)] + [InlineData(2, 16)] + [InlineData(3, 128)] + public void BulkConvertNormalizedFloatToByteClampOverflows(int seed, int count) + { + float[] orig = new Random(seed).GenerateRandomRoundedFloatArray(count, -50, 444); + float[] normalized = orig.Select(f => f / 255f).ToArray(); + + byte[] dest = new byte[count]; + + SimdUtils.BulkConvertNormalizedFloatToByteClampOverflows(normalized, dest); + + byte[] expected = orig.Select(f => (byte)Clamp255(f)).ToArray(); + + Assert.Equal(expected, dest); + } + + [Theory] + [InlineData(0)] + [InlineData(7)] + [InlineData(42)] + [InlineData(255)] + [InlineData(256)] + [InlineData(257)] + private void MagicConvertToByte(float value) + { + byte actual = MagicConvert(value / 256f); + byte expected = (byte)value; + + Assert.Equal(expected, actual); + } + + private static byte MagicConvert(float x) + { + float f = 32768.0f + x; + uint i = Unsafe.As(ref f); + return (byte)i; + } + private static void AssertEvenRoundIsCorrect(Vector r, Vector v) { for (int i = 0; i < Vector.Count; i++) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 50746f683..706fa1e20 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -228,6 +228,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg int componentCount, int inputBufferLength, int seed, + float minVal = 0f, float maxVal = 255f) { var rnd = new Random(seed); @@ -238,7 +239,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int j = 0; j < inputBufferLength; j++) { - values[j] = (float)rnd.NextDouble() * maxVal; + values[j] = (float)rnd.NextDouble() * (maxVal-minVal)+minVal; } // no need to dispose when buffer is not array owner diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs index 6ce16de2f..063f42c00 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +// ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Formats.Jpg { using System; @@ -8,7 +9,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using System.Linq; using System.Numerics; + using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; + using SixLabors.ImageSharp.Formats.Jpeg.GolangPort; + using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort; using Xunit; using Xunit.Abstractions; @@ -30,9 +34,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Baseline.Jpeg444, }; - //[Theory] // Benchmark, enable manually - //[MemberData(nameof(DecodeJpegData))] - public void DecodeJpeg(string fileName) + [Theory] // Benchmark, enable manually + [MemberData(nameof(DecodeJpegData))] + public void DecodeJpeg_Original(string fileName) + { + this.DecodeJpegBenchmarkImpl(fileName, new OrigJpegDecoder()); + } + + [Theory] // Benchmark, enable manually + [MemberData(nameof(DecodeJpegData))] + public void DecodeJpeg_PdfJs(string fileName) + { + this.DecodeJpegBenchmarkImpl(fileName, new PdfJsJpegDecoder()); + } + + private void DecodeJpegBenchmarkImpl(string fileName, IImageDecoder decoder) { const int ExecutionCount = 30; @@ -48,11 +64,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg ExecutionCount, () => { - Image img = Image.Load(bytes); + Image img = Image.Load(bytes, decoder); }, // ReSharper disable once ExplicitCallerInfoArgument $"Decode {fileName}"); - } // Benchmark, enable manually! diff --git a/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs b/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs new file mode 100644 index 000000000..9eb051e7a --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs @@ -0,0 +1,32 @@ +using System; + +namespace SixLabors.ImageSharp.Tests +{ + internal static class TestDataGenerator + { + public static float[] GenerateRandomFloatArray(this Random rnd, int length, float minVal, float maxVal) + { + float[] values = new float[length]; + + for (int i = 0; i < length; i++) + { + values[i] = (float)rnd.NextDouble() * (maxVal - minVal) + minVal; + } + + return values; + } + + public static float[] GenerateRandomRoundedFloatArray(this Random rnd, int length, int minVal, int maxValExclusive) + { + float[] values = new float[length]; + + for (int i = 0; i < length; i++) + { + int val = rnd.Next(minVal, maxValExclusive); + values[i] = (float)val; + } + + return values; + } + } +} \ No newline at end of file