From 09818324ba3cea651c14a802c75b11883a7a2831 Mon Sep 17 00:00:00 2001 From: Maxim Shipko Date: Fri, 6 Jun 2025 18:34:43 +0400 Subject: [PATCH 1/6] Reduce the number of memory allocations in lossless WebP encoder --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 31 ++++++----- .../Formats/Webp/Lossless/CostModel.cs | 4 +- .../Formats/Webp/Lossless/HistogramEncoder.cs | 6 +-- .../Formats/Webp/Lossless/PixOrCopy.cs | 45 ++++++---------- .../Formats/Webp/Lossless/Vp8LBackwardRefs.cs | 30 +++++++---- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 54 +++++++++---------- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 4 +- .../Formats/WebP/Vp8LHistogramTests.cs | 12 ++--- 8 files changed, 86 insertions(+), 100 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 2e7dd722f..7a2679173 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -149,9 +149,9 @@ internal static class BackwardReferenceEncoder } // Find the cacheBits giving the lowest entropy. - for (int idx = 0; idx < refs.Refs.Count; idx++) + for (int idx = 0; idx < refs.Count; idx++) { - PixOrCopy v = refs.Refs[idx]; + PixOrCopy v = refs[idx]; if (v.IsLiteral()) { uint pix = bgra[pos++]; @@ -387,7 +387,7 @@ internal static class BackwardReferenceEncoder colorCache = new ColorCache(cacheBits); } - backwardRefs.Refs.Clear(); + backwardRefs.Clear(); for (int ix = 0; ix < chosenPathSize; ix++) { int len = chosenPath[ix]; @@ -479,7 +479,7 @@ internal static class BackwardReferenceEncoder colorCache = new ColorCache(cacheBits); } - refs.Refs.Clear(); + refs.Clear(); for (int i = 0; i < pixCount;) { // Alternative #1: Code the pixels starting at 'i' using backward reference. @@ -734,7 +734,7 @@ internal static class BackwardReferenceEncoder colorCache = new ColorCache(cacheBits); } - refs.Refs.Clear(); + refs.Clear(); // Add first pixel as literal. AddSingleLiteral(bgra[0], useColorCache, colorCache, refs); @@ -779,10 +779,10 @@ internal static class BackwardReferenceEncoder private static void BackwardRefsWithLocalCache(ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) { int pixelIndex = 0; - ColorCache colorCache = new ColorCache(cacheBits); - for (int idx = 0; idx < refs.Refs.Count; idx++) + ColorCache colorCache = new(cacheBits); + for (int idx = 0; idx < refs.Count; idx++) { - PixOrCopy v = refs.Refs[idx]; + PixOrCopy v = refs[idx]; if (v.IsLiteral()) { uint bgraLiteral = v.BgraOrDistance; @@ -790,9 +790,7 @@ internal static class BackwardReferenceEncoder if (ix >= 0) { // Color cache contains bgraLiteral - v.Mode = PixOrCopyMode.CacheIdx; - v.BgraOrDistance = (uint)ix; - v.Len = 1; + refs[idx] = PixOrCopy.CreateCacheIdx(ix); } else { @@ -814,14 +812,15 @@ internal static class BackwardReferenceEncoder private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs) { - using List.Enumerator c = refs.Refs.GetEnumerator(); - while (c.MoveNext()) + for (int idx = 0; idx < refs.Count; idx++) { - if (c.Current.IsCopy()) + PixOrCopy v = refs[idx]; + + if (v.IsCopy()) { - int dist = (int)c.Current.BgraOrDistance; + int dist = (int)v.BgraOrDistance; int transformedDist = DistanceToPlaneCode(xSize, dist); - c.Current.BgraOrDistance = (uint)transformedDist; + refs[idx] = PixOrCopy.CreateCopy((uint)transformedDist, v.Len); } } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index beebc48ab..15b0f4222 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -40,9 +40,9 @@ internal class CostModel using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. - for (int i = 0; i < backwardRefs.Refs.Count; i++) + for (int i = 0; i < backwardRefs.Count; i++) { - histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize); + histogram.AddSinglePixOrCopy(backwardRefs[i], true, xSize); } ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 3a96362cf..c5d1aa145 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -109,10 +109,10 @@ internal static class HistogramEncoder { int x = 0, y = 0; int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits); - using List.Enumerator backwardRefsEnumerator = backwardRefs.Refs.GetEnumerator(); - while (backwardRefsEnumerator.MoveNext()) + + for (int i = 0; i < backwardRefs.Count; i++) { - PixOrCopy v = backwardRefsEnumerator.Current; + PixOrCopy v = backwardRefs[i]; int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits); histograms[ix].AddSinglePixOrCopy(v, false); x += v.Len; diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs index d6b10ada5..bb8ce18aa 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -6,37 +6,24 @@ using System.Diagnostics; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; [DebuggerDisplay("Mode: {Mode}, Len: {Len}, BgraOrDistance: {BgraOrDistance}")] -internal sealed class PixOrCopy +internal readonly struct PixOrCopy { - public PixOrCopyMode Mode { get; set; } - - public ushort Len { get; set; } - - public uint BgraOrDistance { get; set; } - - public static PixOrCopy CreateCacheIdx(int idx) => - new PixOrCopy - { - Mode = PixOrCopyMode.CacheIdx, - BgraOrDistance = (uint)idx, - Len = 1 - }; - - public static PixOrCopy CreateLiteral(uint bgra) => - new PixOrCopy - { - Mode = PixOrCopyMode.Literal, - BgraOrDistance = bgra, - Len = 1 - }; - - public static PixOrCopy CreateCopy(uint distance, ushort len) => - new PixOrCopy + public readonly PixOrCopyMode Mode; + public readonly ushort Len; + public readonly uint BgraOrDistance; + + private PixOrCopy(PixOrCopyMode mode, ushort len, uint bgraOrDistance) { - Mode = PixOrCopyMode.Copy, - BgraOrDistance = distance, - Len = len - }; + this.Mode = mode; + this.Len = len; + this.BgraOrDistance = bgraOrDistance; + } + + public static PixOrCopy CreateCacheIdx(int idx) => new(PixOrCopyMode.CacheIdx, 1, (uint)idx); + + public static PixOrCopy CreateLiteral(uint bgra) => new(PixOrCopyMode.Literal, 1, bgra); + + public static PixOrCopy CreateCopy(uint distance, ushort len) => new(PixOrCopyMode.Copy, len, distance); public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs index ace9d6227..6a2869995 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs @@ -1,21 +1,29 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal class Vp8LBackwardRefs +internal class Vp8LBackwardRefs : IDisposable { - public Vp8LBackwardRefs(int pixels) => this.Refs = new List(pixels); + private readonly IMemoryOwner refs; + + public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels) + { + this.refs = memoryAllocator.Allocate(pixels); + this.Count = 0; + } + + public int Count { get; private set; } + + public ref PixOrCopy this[int index] => ref this.refs.Memory.Span[index]; - /// - /// Gets or sets the common block-size. - /// - public int BlockSize { get; set; } + public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.Count++] = pixOrCopy; - /// - /// Gets the backward references. - /// - public List Refs { get; } + public void Clear() => this.Count = 0; - public void Add(PixOrCopy pixOrCopy) => this.Refs.Add(pixOrCopy); + /// + public void Dispose() => this.refs.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 1864e539c..e19126fff 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -26,9 +26,9 @@ internal class Vp8LEncoder : IDisposable /// private ScratchBuffer scratch; // mutable struct, don't make readonly - private readonly int[][] histoArgb = { new int[256], new int[256], new int[256], new int[256] }; + private readonly int[][] histoArgb = [new int[256], new int[256], new int[256], new int[256]]; - private readonly int[][] bestHisto = { new int[256], new int[256], new int[256], new int[256] }; + private readonly int[][] bestHisto = [new int[256], new int[256], new int[256], new int[256]]; /// /// The to use for buffer allocations. @@ -45,11 +45,6 @@ internal class Vp8LEncoder : IDisposable /// private const int MaxRefsBlockPerImage = 16; - /// - /// Minimum block size for backward references. - /// - private const int MinBlockSize = 256; - /// /// A bit writer for writing lossless webp streams. /// @@ -136,14 +131,9 @@ internal class Vp8LEncoder : IDisposable this.Refs = new Vp8LBackwardRefs[3]; this.HashChain = new Vp8LHashChain(memoryAllocator, pixelCount); - // We round the block size up, so we're guaranteed to have at most MaxRefsBlockPerImage blocks used: - int refsBlockSize = ((pixelCount - 1) / MaxRefsBlockPerImage) + 1; for (int i = 0; i < this.Refs.Length; i++) { - this.Refs[i] = new Vp8LBackwardRefs(pixelCount) - { - BlockSize = refsBlockSize < MinBlockSize ? MinBlockSize : refsBlockSize - }; + this.Refs[i] = new Vp8LBackwardRefs(memoryAllocator, pixelCount); } } @@ -151,10 +141,10 @@ internal class Vp8LEncoder : IDisposable // This sequence is tuned from that, but more weighted for lower symbol count, // and more spiking histograms. // This uses C#'s compiler optimization to refer to assembly's static data directly. - private static ReadOnlySpan StorageOrder => new byte[] { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + private static ReadOnlySpan StorageOrder => [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; // This uses C#'s compiler optimization to refer to assembly's static data directly. - private static ReadOnlySpan Order => new byte[] { 1, 2, 0, 3 }; + private static ReadOnlySpan Order => [1, 2, 0, 3]; /// /// Gets the memory for the image data as packed bgra values. @@ -547,7 +537,7 @@ internal class Vp8LEncoder : IDisposable EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new(); + List crunchConfigs = []; if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -593,7 +583,7 @@ internal class Vp8LEncoder : IDisposable } } - return crunchConfigs.ToArray(); + return [.. crunchConfigs]; } private void EncodeImage(int width, int height, bool useCache, CrunchConfig config, int cacheBits, bool lowEffort) @@ -1068,9 +1058,9 @@ internal class Vp8LEncoder : IDisposable int histogramIx = histogramSymbols[0]; Span codes = huffmanCodes.AsSpan(5 * histogramIx); - for (int i = 0; i < backwardRefs.Refs.Count; i++) + for (int i = 0; i < backwardRefs.Count; i++) { - PixOrCopy v = backwardRefs.Refs[i]; + PixOrCopy v = backwardRefs[i]; if (tileX != (x & tileMask) || tileY != (y & tileMask)) { tileX = x & tileMask; @@ -1265,13 +1255,13 @@ internal class Vp8LEncoder : IDisposable // non-zero red and blue values. If all are zero, we can later skip // the cross color optimization. byte[][] histoPairs = - { - new[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue }, - new[] { (byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred }, - new[] { (byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen }, - new[] { (byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen }, - new[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue } - }; + [ + [(byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue], + [(byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred], + [(byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen], + [(byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen], + [(byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue] + ]; Span redHisto = histo[(256 * histoPairs[(int)minEntropyIx][0])..]; Span blueHisto = histo[(256 * histoPairs[(int)minEntropyIx][1])..]; for (int i = 1; i < 256; i++) @@ -1325,7 +1315,7 @@ internal class Vp8LEncoder : IDisposable /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new(); + HashSet colors = []; for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); @@ -1904,9 +1894,9 @@ internal class Vp8LEncoder : IDisposable /// public void ClearRefs() { - foreach (Vp8LBackwardRefs t in this.Refs) + foreach (Vp8LBackwardRefs refs in this.Refs) { - t.Refs.Clear(); + refs.Clear(); } } @@ -1918,6 +1908,12 @@ internal class Vp8LEncoder : IDisposable this.BgraScratch?.Dispose(); this.Palette.Dispose(); this.TransformData?.Dispose(); + + foreach (Vp8LBackwardRefs refs in this.Refs) + { + refs.Dispose(); + } + this.HashChain.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index f47397790..ee03499cf 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -138,9 +138,9 @@ internal abstract unsafe class Vp8LHistogram /// The backward references. public void StoreRefs(Vp8LBackwardRefs refs) { - for (int i = 0; i < refs.Refs.Count; i++) + for (int i = 0; i < refs.Count; i++) { - this.AddSinglePixOrCopy(refs.Refs[i], false); + this.AddSinglePixOrCopy(refs[i], false); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index cfe79e49e..7777c6108 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -66,18 +66,14 @@ public class Vp8LHistogramTests // All remaining values are expected to be zero. literals.AsSpan().CopyTo(expectedLiterals); - Vp8LBackwardRefs backwardRefs = new(pixelData.Length); + MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; + + using Vp8LBackwardRefs backwardRefs = new(memoryAllocator, pixelData.Length); for (int i = 0; i < pixelData.Length; i++) { - backwardRefs.Add(new PixOrCopy() - { - BgraOrDistance = pixelData[i], - Len = 1, - Mode = PixOrCopyMode.Literal - }); + backwardRefs.Add(PixOrCopy.CreateLiteral(pixelData[i])); } - MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3); using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) From ff9d3a6223c72ca1c5931593e404ce0070db9d9c Mon Sep 17 00:00:00 2001 From: Maxim Shipko Date: Fri, 6 Jun 2025 19:01:01 +0400 Subject: [PATCH 2/6] Replace indexed span iteration with foreach (+performance) --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 14 +++++--------- src/ImageSharp/Formats/Webp/Lossless/CostModel.cs | 4 ++-- .../Formats/Webp/Lossless/HistogramEncoder.cs | 9 ++++----- .../Formats/Webp/Lossless/Vp8LBackwardRefs.cs | 11 +++++------ .../Formats/Webp/Lossless/Vp8LEncoder.cs | 3 +-- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 4 ++-- 6 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 7a2679173..274d4426f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -149,9 +149,8 @@ internal static class BackwardReferenceEncoder } // Find the cacheBits giving the lowest entropy. - for (int idx = 0; idx < refs.Count; idx++) + foreach (PixOrCopy v in refs) { - PixOrCopy v = refs[idx]; if (v.IsLiteral()) { uint pix = bgra[pos++]; @@ -780,9 +779,8 @@ internal static class BackwardReferenceEncoder { int pixelIndex = 0; ColorCache colorCache = new(cacheBits); - for (int idx = 0; idx < refs.Count; idx++) + foreach (ref PixOrCopy v in refs) { - PixOrCopy v = refs[idx]; if (v.IsLiteral()) { uint bgraLiteral = v.BgraOrDistance; @@ -790,7 +788,7 @@ internal static class BackwardReferenceEncoder if (ix >= 0) { // Color cache contains bgraLiteral - refs[idx] = PixOrCopy.CreateCacheIdx(ix); + v = PixOrCopy.CreateCacheIdx(ix); } else { @@ -812,15 +810,13 @@ internal static class BackwardReferenceEncoder private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs) { - for (int idx = 0; idx < refs.Count; idx++) + foreach (ref PixOrCopy v in refs) { - PixOrCopy v = refs[idx]; - if (v.IsCopy()) { int dist = (int)v.BgraOrDistance; int transformedDist = DistanceToPlaneCode(xSize, dist); - refs[idx] = PixOrCopy.CreateCopy((uint)transformedDist, v.Len); + v = PixOrCopy.CreateCopy((uint)transformedDist, v.Len); } } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index 15b0f4222..56a5bb7f5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -40,9 +40,9 @@ internal class CostModel using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. - for (int i = 0; i < backwardRefs.Count; i++) + foreach (PixOrCopy v in backwardRefs) { - histogram.AddSinglePixOrCopy(backwardRefs[i], true, xSize); + histogram.AddSinglePixOrCopy(v, true, xSize); } ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index c5d1aa145..b60663fb6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -110,9 +110,8 @@ internal static class HistogramEncoder int x = 0, y = 0; int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits); - for (int i = 0; i < backwardRefs.Count; i++) + foreach (PixOrCopy v in backwardRefs) { - PixOrCopy v = backwardRefs[i]; int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits); histograms[ix].AddSinglePixOrCopy(v, false); x += v.Len; @@ -217,7 +216,7 @@ internal static class HistogramEncoder clusterMappings[idx] = (ushort)idx; } - List indicesToRemove = new(); + List indicesToRemove = []; Vp8LStreaks stats = new(); Vp8LBitEntropy bitsEntropy = new(); for (int idx = 0; idx < histograms.Count; idx++) @@ -345,7 +344,7 @@ internal static class HistogramEncoder // Priority list of histogram pairs. Its size impacts the quality of the compression and the speed: // the smaller the faster but the worse for the compression. - List histoPriorityList = new(); + List histoPriorityList = []; const int maxSize = 9; // Fill the initial mapping. @@ -480,7 +479,7 @@ internal static class HistogramEncoder int histoSize = histograms.Count(h => h != null); // Priority list of histogram pairs. - List histoPriorityList = new(); + List histoPriorityList = []; int maxSize = histoSize * histoSize; Vp8LStreaks stats = new(); Vp8LBitEntropy bitsEntropy = new(); diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs index 6a2869995..634fac5e8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs @@ -9,20 +9,19 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal class Vp8LBackwardRefs : IDisposable { private readonly IMemoryOwner refs; + private int count; public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels) { this.refs = memoryAllocator.Allocate(pixels); - this.Count = 0; + this.count = 0; } - public int Count { get; private set; } + public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.count++] = pixOrCopy; - public ref PixOrCopy this[int index] => ref this.refs.Memory.Span[index]; + public void Clear() => this.count = 0; - public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.Count++] = pixOrCopy; - - public void Clear() => this.Count = 0; + public Span.Enumerator GetEnumerator() => this.refs.Slice(0, this.count).GetEnumerator(); /// public void Dispose() => this.refs.Dispose(); diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index e19126fff..b398554eb 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -1058,9 +1058,8 @@ internal class Vp8LEncoder : IDisposable int histogramIx = histogramSymbols[0]; Span codes = huffmanCodes.AsSpan(5 * histogramIx); - for (int i = 0; i < backwardRefs.Count; i++) + foreach (PixOrCopy v in backwardRefs) { - PixOrCopy v = backwardRefs[i]; if (tileX != (x & tileMask) || tileY != (y & tileMask)) { tileX = x & tileMask; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index ee03499cf..399af7661 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -138,9 +138,9 @@ internal abstract unsafe class Vp8LHistogram /// The backward references. public void StoreRefs(Vp8LBackwardRefs refs) { - for (int i = 0; i < refs.Count; i++) + foreach (PixOrCopy v in refs) { - this.AddSinglePixOrCopy(refs[i], false); + this.AddSinglePixOrCopy(v, false); } } From 0faf614b0743b430a8513679be53c9c67537fd04 Mon Sep 17 00:00:00 2001 From: Maxim Shipko Date: Tue, 10 Jun 2025 17:14:42 +0400 Subject: [PATCH 3/6] Change AddSinglePixOrCopy to accept 'in PixOrCopy' --- src/ImageSharp/Formats/Webp/Lossless/CostModel.cs | 2 +- src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs | 2 +- src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index 56a5bb7f5..c6131bc2a 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -42,7 +42,7 @@ internal class CostModel // The following code is similar to HistogramCreate but converts the distance to plane code. foreach (PixOrCopy v in backwardRefs) { - histogram.AddSinglePixOrCopy(v, true, xSize); + histogram.AddSinglePixOrCopy(in v, true, xSize); } ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index b60663fb6..0b610302f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -113,7 +113,7 @@ internal static class HistogramEncoder foreach (PixOrCopy v in backwardRefs) { int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits); - histograms[ix].AddSinglePixOrCopy(v, false); + histograms[ix].AddSinglePixOrCopy(in v, false); x += v.Len; while (x >= xSize) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 399af7661..03bedfe67 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -140,7 +140,7 @@ internal abstract unsafe class Vp8LHistogram { foreach (PixOrCopy v in refs) { - this.AddSinglePixOrCopy(v, false); + this.AddSinglePixOrCopy(in v, false); } } @@ -150,7 +150,7 @@ internal abstract unsafe class Vp8LHistogram /// The token to add. /// Indicates whether to use the distance modifier. /// xSize is only used when useDistanceModifier is true. - public void AddSinglePixOrCopy(PixOrCopy v, bool useDistanceModifier, int xSize = 0) + public void AddSinglePixOrCopy(in PixOrCopy v, bool useDistanceModifier, int xSize = 0) { if (v.IsLiteral()) { From b240679c71db329f8977545a4835f8a452771b02 Mon Sep 17 00:00:00 2001 From: Maxim Shipko Date: Tue, 10 Jun 2025 17:51:12 +0400 Subject: [PATCH 4/6] Pin the refs for faster access --- .../Formats/Webp/Lossless/Vp8LBackwardRefs.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs index 634fac5e8..3b7dc99ff 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs @@ -8,21 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal class Vp8LBackwardRefs : IDisposable { - private readonly IMemoryOwner refs; + private readonly IMemoryOwner owner; + private readonly MemoryHandle handle; private int count; public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels) { - this.refs = memoryAllocator.Allocate(pixels); + this.owner = memoryAllocator.Allocate(pixels); + this.handle = this.owner.Memory.Pin(); this.count = 0; } - public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.count++] = pixOrCopy; + public void Add(PixOrCopy pixOrCopy) + { + unsafe + { + ((PixOrCopy*)this.handle.Pointer)[this.count++] = pixOrCopy; + } + } public void Clear() => this.count = 0; - public Span.Enumerator GetEnumerator() => this.refs.Slice(0, this.count).GetEnumerator(); + public Span.Enumerator GetEnumerator() => this.owner.Slice(0, this.count).GetEnumerator(); /// - public void Dispose() => this.refs.Dispose(); + public void Dispose() + { + this.handle.Dispose(); + this.owner.Dispose(); + } } From 909e06be69ddfbbe8880fd6a766b7ef67a08b248 Mon Sep 17 00:00:00 2001 From: Maxim Shipko Date: Tue, 10 Jun 2025 18:44:51 +0400 Subject: [PATCH 5/6] Revert "Pin the refs for faster access" --- .../Formats/Webp/Lossless/Vp8LBackwardRefs.cs | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs index 3b7dc99ff..634fac5e8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs @@ -8,33 +8,21 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal class Vp8LBackwardRefs : IDisposable { - private readonly IMemoryOwner owner; - private readonly MemoryHandle handle; + private readonly IMemoryOwner refs; private int count; public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels) { - this.owner = memoryAllocator.Allocate(pixels); - this.handle = this.owner.Memory.Pin(); + this.refs = memoryAllocator.Allocate(pixels); this.count = 0; } - public void Add(PixOrCopy pixOrCopy) - { - unsafe - { - ((PixOrCopy*)this.handle.Pointer)[this.count++] = pixOrCopy; - } - } + public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.count++] = pixOrCopy; public void Clear() => this.count = 0; - public Span.Enumerator GetEnumerator() => this.owner.Slice(0, this.count).GetEnumerator(); + public Span.Enumerator GetEnumerator() => this.refs.Slice(0, this.count).GetEnumerator(); /// - public void Dispose() - { - this.handle.Dispose(); - this.owner.Dispose(); - } + public void Dispose() => this.refs.Dispose(); } From 17fc6dbce95f58f3dc2059250ea649ed1dc61f74 Mon Sep 17 00:00:00 2001 From: Maxim Shipko Date: Wed, 11 Jun 2025 03:57:36 +0400 Subject: [PATCH 6/6] Remove list search in HistoListUpdateHead, we always know pair index at call site --- .../Formats/Webp/Lossless/HistogramEncoder.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 0b610302f..e0d854bb0 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -464,7 +464,7 @@ internal static class HistogramEncoder } } - HistoListUpdateHead(histoPriorityList, p); + HistoListUpdateHead(histoPriorityList, p, j); j++; } @@ -524,7 +524,7 @@ internal static class HistogramEncoder } else { - HistoListUpdateHead(histoPriorityList, p); + HistoListUpdateHead(histoPriorityList, p, i); i++; } } @@ -646,7 +646,7 @@ internal static class HistogramEncoder histoList.Add(pair); - HistoListUpdateHead(histoList, pair); + HistoListUpdateHead(histoList, pair, histoList.Count - 1); return pair.CostDiff; } @@ -673,13 +673,11 @@ internal static class HistogramEncoder /// /// Check whether a pair in the list should be updated as head or not. /// - private static void HistoListUpdateHead(List histoList, HistogramPair pair) + private static void HistoListUpdateHead(List histoList, HistogramPair pair, int idx) { if (pair.CostDiff < histoList[0].CostDiff) { - // Replace the best pair. - int oldIdx = histoList.IndexOf(pair); - histoList[oldIdx] = histoList[0]; + histoList[idx] = histoList[0]; histoList[0] = pair; } }