Skip to content

Commit 0981832

Browse files
committed
Reduce the number of memory allocations in lossless WebP encoder
1 parent c82cb24 commit 0981832

File tree

8 files changed

+86
-100
lines changed

8 files changed

+86
-100
lines changed

src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,9 @@ private static int CalculateBestCacheSize(
149149
}
150150

151151
// Find the cacheBits giving the lowest entropy.
152-
for (int idx = 0; idx < refs.Refs.Count; idx++)
152+
for (int idx = 0; idx < refs.Count; idx++)
153153
{
154-
PixOrCopy v = refs.Refs[idx];
154+
PixOrCopy v = refs[idx];
155155
if (v.IsLiteral())
156156
{
157157
uint pix = bgra[pos++];
@@ -387,7 +387,7 @@ private static void BackwardReferencesHashChainFollowChosenPath(ReadOnlySpan<uin
387387
colorCache = new ColorCache(cacheBits);
388388
}
389389

390-
backwardRefs.Refs.Clear();
390+
backwardRefs.Clear();
391391
for (int ix = 0; ix < chosenPathSize; ix++)
392392
{
393393
int len = chosenPath[ix];
@@ -479,7 +479,7 @@ private static void BackwardReferencesLz77(int xSize, int ySize, ReadOnlySpan<ui
479479
colorCache = new ColorCache(cacheBits);
480480
}
481481

482-
refs.Refs.Clear();
482+
refs.Clear();
483483
for (int i = 0; i < pixCount;)
484484
{
485485
// Alternative #1: Code the pixels starting at 'i' using backward reference.
@@ -734,7 +734,7 @@ private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan<uin
734734
colorCache = new ColorCache(cacheBits);
735735
}
736736

737-
refs.Refs.Clear();
737+
refs.Clear();
738738

739739
// Add first pixel as literal.
740740
AddSingleLiteral(bgra[0], useColorCache, colorCache, refs);
@@ -779,20 +779,18 @@ private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan<uin
779779
private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> bgra, int cacheBits, Vp8LBackwardRefs refs)
780780
{
781781
int pixelIndex = 0;
782-
ColorCache colorCache = new ColorCache(cacheBits);
783-
for (int idx = 0; idx < refs.Refs.Count; idx++)
782+
ColorCache colorCache = new(cacheBits);
783+
for (int idx = 0; idx < refs.Count; idx++)
784784
{
785-
PixOrCopy v = refs.Refs[idx];
785+
PixOrCopy v = refs[idx];
786786
if (v.IsLiteral())
787787
{
788788
uint bgraLiteral = v.BgraOrDistance;
789789
int ix = colorCache.Contains(bgraLiteral);
790790
if (ix >= 0)
791791
{
792792
// Color cache contains bgraLiteral
793-
v.Mode = PixOrCopyMode.CacheIdx;
794-
v.BgraOrDistance = (uint)ix;
795-
v.Len = 1;
793+
refs[idx] = PixOrCopy.CreateCacheIdx(ix);
796794
}
797795
else
798796
{
@@ -814,14 +812,15 @@ private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> bgra, int cach
814812

815813
private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs)
816814
{
817-
using List<PixOrCopy>.Enumerator c = refs.Refs.GetEnumerator();
818-
while (c.MoveNext())
815+
for (int idx = 0; idx < refs.Count; idx++)
819816
{
820-
if (c.Current.IsCopy())
817+
PixOrCopy v = refs[idx];
818+
819+
if (v.IsCopy())
821820
{
822-
int dist = (int)c.Current.BgraOrDistance;
821+
int dist = (int)v.BgraOrDistance;
823822
int transformedDist = DistanceToPlaneCode(xSize, dist);
824-
c.Current.BgraOrDistance = (uint)transformedDist;
823+
refs[idx] = PixOrCopy.CreateCopy((uint)transformedDist, v.Len);
825824
}
826825
}
827826
}

src/ImageSharp/Formats/Webp/Lossless/CostModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs)
4040
using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits);
4141

4242
// The following code is similar to HistogramCreate but converts the distance to plane code.
43-
for (int i = 0; i < backwardRefs.Refs.Count; i++)
43+
for (int i = 0; i < backwardRefs.Count; i++)
4444
{
45-
histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize);
45+
histogram.AddSinglePixOrCopy(backwardRefs[i], true, xSize);
4646
}
4747

4848
ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal);

src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ private static void HistogramBuild(
109109
{
110110
int x = 0, y = 0;
111111
int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
112-
using List<PixOrCopy>.Enumerator backwardRefsEnumerator = backwardRefs.Refs.GetEnumerator();
113-
while (backwardRefsEnumerator.MoveNext())
112+
113+
for (int i = 0; i < backwardRefs.Count; i++)
114114
{
115-
PixOrCopy v = backwardRefsEnumerator.Current;
115+
PixOrCopy v = backwardRefs[i];
116116
int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits);
117117
histograms[ix].AddSinglePixOrCopy(v, false);
118118
x += v.Len;

src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,24 @@
66
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
77

88
[DebuggerDisplay("Mode: {Mode}, Len: {Len}, BgraOrDistance: {BgraOrDistance}")]
9-
internal sealed class PixOrCopy
9+
internal readonly struct PixOrCopy
1010
{
11-
public PixOrCopyMode Mode { get; set; }
12-
13-
public ushort Len { get; set; }
14-
15-
public uint BgraOrDistance { get; set; }
16-
17-
public static PixOrCopy CreateCacheIdx(int idx) =>
18-
new PixOrCopy
19-
{
20-
Mode = PixOrCopyMode.CacheIdx,
21-
BgraOrDistance = (uint)idx,
22-
Len = 1
23-
};
24-
25-
public static PixOrCopy CreateLiteral(uint bgra) =>
26-
new PixOrCopy
27-
{
28-
Mode = PixOrCopyMode.Literal,
29-
BgraOrDistance = bgra,
30-
Len = 1
31-
};
32-
33-
public static PixOrCopy CreateCopy(uint distance, ushort len) =>
34-
new PixOrCopy
11+
public readonly PixOrCopyMode Mode;
12+
public readonly ushort Len;
13+
public readonly uint BgraOrDistance;
14+
15+
private PixOrCopy(PixOrCopyMode mode, ushort len, uint bgraOrDistance)
3516
{
36-
Mode = PixOrCopyMode.Copy,
37-
BgraOrDistance = distance,
38-
Len = len
39-
};
17+
this.Mode = mode;
18+
this.Len = len;
19+
this.BgraOrDistance = bgraOrDistance;
20+
}
21+
22+
public static PixOrCopy CreateCacheIdx(int idx) => new(PixOrCopyMode.CacheIdx, 1, (uint)idx);
23+
24+
public static PixOrCopy CreateLiteral(uint bgra) => new(PixOrCopyMode.Literal, 1, bgra);
25+
26+
public static PixOrCopy CreateCopy(uint distance, ushort len) => new(PixOrCopyMode.Copy, len, distance);
4027

4128
public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF;
4229

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Buffers;
5+
using SixLabors.ImageSharp.Memory;
6+
47
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
58

6-
internal class Vp8LBackwardRefs
9+
internal class Vp8LBackwardRefs : IDisposable
710
{
8-
public Vp8LBackwardRefs(int pixels) => this.Refs = new List<PixOrCopy>(pixels);
11+
private readonly IMemoryOwner<PixOrCopy> refs;
12+
13+
public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels)
14+
{
15+
this.refs = memoryAllocator.Allocate<PixOrCopy>(pixels);
16+
this.Count = 0;
17+
}
18+
19+
public int Count { get; private set; }
20+
21+
public ref PixOrCopy this[int index] => ref this.refs.Memory.Span[index];
922

10-
/// <summary>
11-
/// Gets or sets the common block-size.
12-
/// </summary>
13-
public int BlockSize { get; set; }
23+
public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.Count++] = pixOrCopy;
1424

15-
/// <summary>
16-
/// Gets the backward references.
17-
/// </summary>
18-
public List<PixOrCopy> Refs { get; }
25+
public void Clear() => this.Count = 0;
1926

20-
public void Add(PixOrCopy pixOrCopy) => this.Refs.Add(pixOrCopy);
27+
/// <inheritdoc/>
28+
public void Dispose() => this.refs.Dispose();
2129
}

src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ internal class Vp8LEncoder : IDisposable
2626
/// </summary>
2727
private ScratchBuffer scratch; // mutable struct, don't make readonly
2828

29-
private readonly int[][] histoArgb = { new int[256], new int[256], new int[256], new int[256] };
29+
private readonly int[][] histoArgb = [new int[256], new int[256], new int[256], new int[256]];
3030

31-
private readonly int[][] bestHisto = { new int[256], new int[256], new int[256], new int[256] };
31+
private readonly int[][] bestHisto = [new int[256], new int[256], new int[256], new int[256]];
3232

3333
/// <summary>
3434
/// The <see cref="MemoryAllocator"/> to use for buffer allocations.
@@ -45,11 +45,6 @@ internal class Vp8LEncoder : IDisposable
4545
/// </summary>
4646
private const int MaxRefsBlockPerImage = 16;
4747

48-
/// <summary>
49-
/// Minimum block size for backward references.
50-
/// </summary>
51-
private const int MinBlockSize = 256;
52-
5348
/// <summary>
5449
/// A bit writer for writing lossless webp streams.
5550
/// </summary>
@@ -136,25 +131,20 @@ public Vp8LEncoder(
136131
this.Refs = new Vp8LBackwardRefs[3];
137132
this.HashChain = new Vp8LHashChain(memoryAllocator, pixelCount);
138133

139-
// We round the block size up, so we're guaranteed to have at most MaxRefsBlockPerImage blocks used:
140-
int refsBlockSize = ((pixelCount - 1) / MaxRefsBlockPerImage) + 1;
141134
for (int i = 0; i < this.Refs.Length; i++)
142135
{
143-
this.Refs[i] = new Vp8LBackwardRefs(pixelCount)
144-
{
145-
BlockSize = refsBlockSize < MinBlockSize ? MinBlockSize : refsBlockSize
146-
};
136+
this.Refs[i] = new Vp8LBackwardRefs(memoryAllocator, pixelCount);
147137
}
148138
}
149139

150140
// RFC 1951 will calm you down if you are worried about this funny sequence.
151141
// This sequence is tuned from that, but more weighted for lower symbol count,
152142
// and more spiking histograms.
153143
// This uses C#'s compiler optimization to refer to assembly's static data directly.
154-
private static ReadOnlySpan<byte> StorageOrder => new byte[] { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
144+
private static ReadOnlySpan<byte> StorageOrder => [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
155145

156146
// This uses C#'s compiler optimization to refer to assembly's static data directly.
157-
private static ReadOnlySpan<byte> Order => new byte[] { 1, 2, 0, 3 };
147+
private static ReadOnlySpan<byte> Order => [1, 2, 0, 3];
158148

159149
/// <summary>
160150
/// Gets the memory for the image data as packed bgra values.
@@ -547,7 +537,7 @@ private CrunchConfig[] EncoderAnalyze(ReadOnlySpan<uint> bgra, int width, int he
547537
EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero);
548538

549539
bool doNotCache = false;
550-
List<CrunchConfig> crunchConfigs = new();
540+
List<CrunchConfig> crunchConfigs = [];
551541

552542
if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100)
553543
{
@@ -593,7 +583,7 @@ private CrunchConfig[] EncoderAnalyze(ReadOnlySpan<uint> bgra, int width, int he
593583
}
594584
}
595585

596-
return crunchConfigs.ToArray();
586+
return [.. crunchConfigs];
597587
}
598588

599589
private void EncodeImage(int width, int height, bool useCache, CrunchConfig config, int cacheBits, bool lowEffort)
@@ -1068,9 +1058,9 @@ private void StoreImageToBitMask(
10681058
int histogramIx = histogramSymbols[0];
10691059
Span<HuffmanTreeCode> codes = huffmanCodes.AsSpan(5 * histogramIx);
10701060

1071-
for (int i = 0; i < backwardRefs.Refs.Count; i++)
1061+
for (int i = 0; i < backwardRefs.Count; i++)
10721062
{
1073-
PixOrCopy v = backwardRefs.Refs[i];
1063+
PixOrCopy v = backwardRefs[i];
10741064
if (tileX != (x & tileMask) || tileY != (y & tileMask))
10751065
{
10761066
tileX = x & tileMask;
@@ -1265,13 +1255,13 @@ private EntropyIx AnalyzeEntropy(ReadOnlySpan<uint> bgra, int width, int height,
12651255
// non-zero red and blue values. If all are zero, we can later skip
12661256
// the cross color optimization.
12671257
byte[][] histoPairs =
1268-
{
1269-
new[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue },
1270-
new[] { (byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred },
1271-
new[] { (byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen },
1272-
new[] { (byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen },
1273-
new[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue }
1274-
};
1258+
[
1259+
[(byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue],
1260+
[(byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred],
1261+
[(byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen],
1262+
[(byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen],
1263+
[(byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue]
1264+
];
12751265
Span<uint> redHisto = histo[(256 * histoPairs[(int)minEntropyIx][0])..];
12761266
Span<uint> blueHisto = histo[(256 * histoPairs[(int)minEntropyIx][1])..];
12771267
for (int i = 1; i < 256; i++)
@@ -1325,7 +1315,7 @@ private bool AnalyzeAndCreatePalette(ReadOnlySpan<uint> bgra, int width, int hei
13251315
/// <returns>The number of palette entries.</returns>
13261316
private static int GetColorPalette(ReadOnlySpan<uint> bgra, int width, int height, Span<uint> palette)
13271317
{
1328-
HashSet<uint> colors = new();
1318+
HashSet<uint> colors = [];
13291319
for (int y = 0; y < height; y++)
13301320
{
13311321
ReadOnlySpan<uint> bgraRow = bgra.Slice(y * width, width);
@@ -1904,9 +1894,9 @@ public void AllocateTransformBuffer(int width, int height)
19041894
/// </summary>
19051895
public void ClearRefs()
19061896
{
1907-
foreach (Vp8LBackwardRefs t in this.Refs)
1897+
foreach (Vp8LBackwardRefs refs in this.Refs)
19081898
{
1909-
t.Refs.Clear();
1899+
refs.Clear();
19101900
}
19111901
}
19121902

@@ -1918,6 +1908,12 @@ public void Dispose()
19181908
this.BgraScratch?.Dispose();
19191909
this.Palette.Dispose();
19201910
this.TransformData?.Dispose();
1911+
1912+
foreach (Vp8LBackwardRefs refs in this.Refs)
1913+
{
1914+
refs.Dispose();
1915+
}
1916+
19211917
this.HashChain.Dispose();
19221918
}
19231919

src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ public void Clear()
138138
/// <param name="refs">The backward references.</param>
139139
public void StoreRefs(Vp8LBackwardRefs refs)
140140
{
141-
for (int i = 0; i < refs.Refs.Count; i++)
141+
for (int i = 0; i < refs.Count; i++)
142142
{
143-
this.AddSinglePixOrCopy(refs.Refs[i], false);
143+
this.AddSinglePixOrCopy(refs[i], false);
144144
}
145145
}
146146

tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,14 @@ private static void RunAddVectorTest()
6666
// All remaining values are expected to be zero.
6767
literals.AsSpan().CopyTo(expectedLiterals);
6868

69-
Vp8LBackwardRefs backwardRefs = new(pixelData.Length);
69+
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
70+
71+
using Vp8LBackwardRefs backwardRefs = new(memoryAllocator, pixelData.Length);
7072
for (int i = 0; i < pixelData.Length; i++)
7173
{
72-
backwardRefs.Add(new PixOrCopy()
73-
{
74-
BgraOrDistance = pixelData[i],
75-
Len = 1,
76-
Mode = PixOrCopyMode.Literal
77-
});
74+
backwardRefs.Add(PixOrCopy.CreateLiteral(pixelData[i]));
7875
}
7976

80-
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
8177
using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
8278
using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
8379
for (int i = 0; i < 5; i++)

0 commit comments

Comments
 (0)