Skip to content

Reduce the number of memory allocations in lossless WebP encoder #2940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,8 @@ private static int CalculateBestCacheSize(
}

// Find the cacheBits giving the lowest entropy.
for (int idx = 0; idx < refs.Refs.Count; idx++)
foreach (PixOrCopy v in refs)
{
PixOrCopy v = refs.Refs[idx];
if (v.IsLiteral())
{
uint pix = bgra[pos++];
Expand Down Expand Up @@ -387,7 +386,7 @@ private static void BackwardReferencesHashChainFollowChosenPath(ReadOnlySpan<uin
colorCache = new ColorCache(cacheBits);
}

backwardRefs.Refs.Clear();
backwardRefs.Clear();
for (int ix = 0; ix < chosenPathSize; ix++)
{
int len = chosenPath[ix];
Expand Down Expand Up @@ -479,7 +478,7 @@ private static void BackwardReferencesLz77(int xSize, int ySize, ReadOnlySpan<ui
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.
Expand Down Expand Up @@ -734,7 +733,7 @@ private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan<uin
colorCache = new ColorCache(cacheBits);
}

refs.Refs.Clear();
refs.Clear();

// Add first pixel as literal.
AddSingleLiteral(bgra[0], useColorCache, colorCache, refs);
Expand Down Expand Up @@ -779,20 +778,17 @@ private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan<uin
private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> 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);
foreach (ref PixOrCopy v in refs)
{
PixOrCopy v = refs.Refs[idx];
if (v.IsLiteral())
{
uint bgraLiteral = v.BgraOrDistance;
int ix = colorCache.Contains(bgraLiteral);
if (ix >= 0)
{
// Color cache contains bgraLiteral
v.Mode = PixOrCopyMode.CacheIdx;
v.BgraOrDistance = (uint)ix;
v.Len = 1;
v = PixOrCopy.CreateCacheIdx(ix);
}
else
{
Expand All @@ -814,14 +810,13 @@ private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> bgra, int cach

private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs)
{
using List<PixOrCopy>.Enumerator c = refs.Refs.GetEnumerator();
while (c.MoveNext())
foreach (ref PixOrCopy v in refs)
{
if (c.Current.IsCopy())
if (v.IsCopy())
{
int dist = (int)c.Current.BgraOrDistance;
int dist = (int)v.BgraOrDistance;
int transformedDist = DistanceToPlaneCode(xSize, dist);
c.Current.BgraOrDistance = (uint)transformedDist;
v = PixOrCopy.CreateCopy((uint)transformedDist, v.Len);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs)
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++)
foreach (PixOrCopy v in backwardRefs)
{
histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize);
histogram.AddSinglePixOrCopy(in v, true, xSize);
}

ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal);
Expand Down
25 changes: 11 additions & 14 deletions src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,11 @@ private static void HistogramBuild(
{
int x = 0, y = 0;
int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
using List<PixOrCopy>.Enumerator backwardRefsEnumerator = backwardRefs.Refs.GetEnumerator();
while (backwardRefsEnumerator.MoveNext())

foreach (PixOrCopy v in backwardRefs)
{
PixOrCopy v = backwardRefsEnumerator.Current;
int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits);
histograms[ix].AddSinglePixOrCopy(v, false);
histograms[ix].AddSinglePixOrCopy(in v, false);
x += v.Len;
while (x >= xSize)
{
Expand Down Expand Up @@ -217,7 +216,7 @@ private static void HistogramCombineEntropyBin(
clusterMappings[idx] = (ushort)idx;
}

List<int> indicesToRemove = new();
List<int> indicesToRemove = [];
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int idx = 0; idx < histograms.Count; idx++)
Expand Down Expand Up @@ -345,7 +344,7 @@ private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int

// 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<HistogramPair> histoPriorityList = new();
List<HistogramPair> histoPriorityList = [];
const int maxSize = 9;

// Fill the initial mapping.
Expand Down Expand Up @@ -465,7 +464,7 @@ private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int
}
}

HistoListUpdateHead(histoPriorityList, p);
HistoListUpdateHead(histoPriorityList, p, j);
j++;
}

Expand All @@ -480,7 +479,7 @@ private static void HistogramCombineGreedy(Vp8LHistogramSet histograms)
int histoSize = histograms.Count(h => h != null);

// Priority list of histogram pairs.
List<HistogramPair> histoPriorityList = new();
List<HistogramPair> histoPriorityList = [];
int maxSize = histoSize * histoSize;
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
Expand Down Expand Up @@ -525,7 +524,7 @@ private static void HistogramCombineGreedy(Vp8LHistogramSet histograms)
}
else
{
HistoListUpdateHead(histoPriorityList, p);
HistoListUpdateHead(histoPriorityList, p, i);
i++;
}
}
Expand Down Expand Up @@ -647,7 +646,7 @@ private static double HistoPriorityListPush(

histoList.Add(pair);

HistoListUpdateHead(histoList, pair);
HistoListUpdateHead(histoList, pair, histoList.Count - 1);

return pair.CostDiff;
}
Expand All @@ -674,13 +673,11 @@ private static void HistoListUpdatePair(
/// <summary>
/// Check whether a pair in the list should be updated as head or not.
/// </summary>
private static void HistoListUpdateHead(List<HistogramPair> histoList, HistogramPair pair)
private static void HistoListUpdateHead(List<HistogramPair> 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;
}
}
Expand Down
45 changes: 16 additions & 29 deletions src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,24 @@
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;

Expand Down
29 changes: 18 additions & 11 deletions src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
// 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<PixOrCopy>(pixels);
private readonly IMemoryOwner<PixOrCopy> refs;
private int count;

public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels)
{
this.refs = memoryAllocator.Allocate<PixOrCopy>(pixels);
this.count = 0;
}

public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.count++] = pixOrCopy;

/// <summary>
/// Gets or sets the common block-size.
/// </summary>
public int BlockSize { get; set; }
public void Clear() => this.count = 0;

/// <summary>
/// Gets the backward references.
/// </summary>
public List<PixOrCopy> Refs { get; }
public Span<PixOrCopy>.Enumerator GetEnumerator() => this.refs.Slice(0, this.count).GetEnumerator();

public void Add(PixOrCopy pixOrCopy) => this.Refs.Add(pixOrCopy);
/// <inheritdoc/>
public void Dispose() => this.refs.Dispose();
}
Loading
Loading